Разработчики

Факс-API, работающий так же привычно, как и весь ваш стек.

REST через HTTPS с JSON, API-ключи bearer, подписанные вебхуки и MCP-сервер, сгенерированный из того же API. Повторный запрос никогда не создаст дубликат факса.

HTTP API

REST и JSON с ключами bearer. Загрузите файл, отправьте с Idempotency-Key, затем опрашивайте статус или ожидайте вебхук.

POST /v1/accounts/{id}/faxes

MCP-сервер

Тот же API в виде инструментов Model Context Protocol, отфильтрованных по скоупам вашего ключа, для факсимильных агентов.

send_fax · get_fax · list_faxes

Вебхуки

Доставки с подписью HMAC-SHA256 и тем же объектом факса, с повторными попытками по расписанию. Игнорируйте дубликаты доставок (сверяйте delivery_id).

fax.received · fax.succeeded

Быстрый старт

Создайте API-ключ в приложении в разделе ключей вашего аккаунта. Ключи имеют вид bfx_live_… или bfx_test_…. Тестовый ключ создает факсы в песочнице: они рендерятся и проверяются как реальные, затем помечаются succeeded без набора номера и списаний. Передавайте ключ в заголовке Authorization: Bearer.

  1. 1. ЗагрузкаPDF, TIFF, JPEG или PNG до 20 МиБ. Значение content_ref действует один час и используется однократно.
  2. 2. Отправкаto в формате E.164. Повторно используйте тот же Idempotency-Key при повторах в течение 24 часов.
  3. 3. ОтслеживаниеОпрашивайте факс или ожидайте событие fax.succeeded либо fax.failed.
Язык быстрого старта
Загрузка, отправка и отслеживание · curl
export FAX_API=https://api.faxdev.com
export FAX_API_KEY=bfx_test_...     # bfx_test_ = sandbox: never dials, never bills
export ACCOUNT_ID=8a1f5c2e-3b7d-4e0a-9c61-2f4d8b7e1a90

# 1. Upload the document (raw body, up to 20 MiB)
curl -sS -X POST "$FAX_API/v1/accounts/$ACCOUNT_ID/uploads" \
  -H "Authorization: Bearer $FAX_API_KEY" \
  -H "Content-Type: application/pdf" \
  -H "Idempotency-Key: upload-invoice-1042" \
  --data-binary @invoice-1042.pdf
# -> { "content_ref": "3f9b2d71-6c4e-4a8b-b0d2-7e5a1c9f8d36", ... }

# 2. Send it
curl -sS -X POST "$FAX_API/v1/accounts/$ACCOUNT_ID/faxes" \
  -H "Authorization: Bearer $FAX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: send-invoice-1042" \
  -d '{
        "to": "+13125550142",
        "content_ref": "3f9b2d71-6c4e-4a8b-b0d2-7e5a1c9f8d36",
        "quality": "fine",
        "client_reference": "invoice-1042",
        "cover": { "enabled": true, "line1": "Invoice 1042", "line2": "Attn: Accounts payable" }
      }'

# 3. Follow it
curl -sS "$FAX_API/v1/accounts/$ACCOUNT_ID/faxes/c0e7a4b2-91d5-4f3e-8a26-5b1c9d0e7f43" \
  -H "Authorization: Bearer $FAX_API_KEY"
Загрузка, отправка и опрос · Python
# pip install requests
import os, time, requests

API = os.environ["FAX_API"]            # https://api.faxdev.com
KEY = os.environ["FAX_API_KEY"]        # bfx_test_... = sandbox
ACCOUNT = os.environ["ACCOUNT_ID"]
auth = {"Authorization": f"Bearer {KEY}"}
base = f"{API}/v1/accounts/{ACCOUNT}"

# 1. Upload the document
with open("invoice-1042.pdf", "rb") as f:
    up = requests.post(f"{base}/uploads", data=f, timeout=60, headers={
        **auth, "Content-Type": "application/pdf", "Idempotency-Key": "upload-invoice-1042"})
up.raise_for_status()

# 2. Send it (reuse the same Idempotency-Key if you retry)
res = requests.post(f"{base}/faxes", timeout=30,
    headers={**auth, "Idempotency-Key": "send-invoice-1042"},
    json={"to": "+13125550142", "content_ref": up.json()["content_ref"],
          "client_reference": "invoice-1042"})
res.raise_for_status()
fax = res.json()

# 3. Poll until it finishes (or wait for the webhook)
DONE = {"succeeded", "failed", "cancelled", "expired", "rejected"}
while fax["status"] not in DONE:
    time.sleep(5)
    fax = requests.get(fax["links"]["self"], headers=auth, timeout=30).json()
print(fax["status"], fax["pages_delivered"], fax["caller_id_delivery"])
Загрузка, отправка и опрос · Node.js
// Node 18+ (global fetch), ES modules
import { readFile } from "node:fs/promises";

const { FAX_API, FAX_API_KEY, ACCOUNT_ID } = process.env; // bfx_test_... = sandbox
const base = `${FAX_API}/v1/accounts/${ACCOUNT_ID}`;
const auth = { Authorization: `Bearer ${FAX_API_KEY}` };

// 1. Upload the document
const up = await fetch(`${base}/uploads`, {
  method: "POST",
  headers: { ...auth, "Content-Type": "application/pdf", "Idempotency-Key": "upload-invoice-1042" },
  body: await readFile("invoice-1042.pdf"),
}).then((r) => r.json());

// 2. Send it (reuse the same Idempotency-Key if you retry)
let fax = await fetch(`${base}/faxes`, {
  method: "POST",
  headers: { ...auth, "Content-Type": "application/json", "Idempotency-Key": "send-invoice-1042" },
  body: JSON.stringify({ to: "+13125550142", content_ref: up.content_ref, client_reference: "invoice-1042" }),
}).then((r) => r.json());

// 3. Poll until it finishes (or wait for the webhook)
const done = new Set(["succeeded", "failed", "cancelled", "expired", "rejected"]);
while (!done.has(fax.status)) {
  await new Promise((r) => setTimeout(r, 5000));
  fax = await fetch(fax.links.self, { headers: auth }).then((r) => r.json());
}
console.log(fax.status, fax.pages_delivered, fax.caller_id_delivery);

Когда документ создан, links.document перенаправляет (302) авторизованный запрос на пятиминутный URL для скачивания. В аккаунтах с журналированием доступа тот же запрос возвращает сам документ (200), а ?redirect=false возвращает одноразовую ссылку, действующую 60 секунд. Каждое чтение документа фиксируется в журнале аудита. Если факс доставлен через резервную сеть оператора, caller_id_delivery принимает значение "not_guaranteed": страницы переданы, но получатель может увидеть другой номер отправителя.

Ответы

Факс проходит этапы queued → rendering → admitted → … и завершается как succeeded, failed, cancelled, expired, или rejected. Вебхук передает тот же объект. Значения статусов:

  • queued, rendering: принят и преобразуется в страницы факса.
  • admitted: проверен и поставлен в очередь на отправку.
  • transmitting: страницы передаются прямо сейчас.
  • retrying: попытка не удалась; следующая уже запланирована, возможно, через другую сеть оператора. Отправлять повторно вручную не нужно.
  • succeeded: доставлен и тарифицирован за фактически переданные страницы. failed, cancelled, expired, а rejected не тарифицируются.

Факс может проходить другие промежуточные статусы при установке соединения. Считайте любой нетерминальный статус выполняющимся, а полную историю попыток смотрите в links.attempts .

Примеры ответов
POST …/faxes → 202 Accepted
HTTP/1.1 202 Accepted
{
  "id": "c0e7a4b2-91d5-4f3e-8a26-5b1c9d0e7f43",
  "status": "queued",
  "direction": "outbound",
  "to": "+13125550142",
  "from": "+14155550100",
  "did_id": "5d2c8e1a-7b4f-4c9d-a3e6-0f1b2c3d4e5f",
  "caller_id_delivery": null,
  "pages": null,
  "pages_delivered": null,
  "retail_cents": null,
  "sandbox": true,
  "read_at": null,
  "failure_class": null,
  "reject_reason": null,
  "client_reference": "invoice-1042",
  "created_at": "2026-10-27T14:04:05Z",
  "completed_at": null,
  "links": {
    "self": "https://api.faxdev.com/v1/accounts/8a1f5c2e-3b7d-4e0a-9c61-2f4d8b7e1a90/faxes/c0e7a4b2-91d5-4f3e-8a26-5b1c9d0e7f43",
    "attempts": "https://api.faxdev.com/v1/accounts/8a1f5c2e-3b7d-4e0a-9c61-2f4d8b7e1a90/faxes/c0e7a4b2-91d5-4f3e-8a26-5b1c9d0e7f43/attempts",
    "document": null
  }
}
POST …/uploads → 201 Created
HTTP/1.1 201 Created
{
  "content_ref": "3f9b2d71-6c4e-4a8b-b0d2-7e5a1c9f8d36",
  "content_type": "application/pdf",
  "bytes": 48213,
  "sha256": "9f2c4e...b71a",
  "expires_at": "2026-10-27T15:04:05Z"
}

Ошибки и повторы

Ошибки возвращаются как application/problem+json со стабильным машиночитаемым полем code. Сетевые ошибки и коды 429 / 5xx можно повторять с тем же Idempotency-Key, учитывая заголовок Retry-After. Не повторяйте другие ответы 4xx без изменения тела запроса.

Документ ошибки
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
  "type": "https://api.faxdev.com/problems/idempotency_conflict",
  "title": "Idempotency key reused with a different request",
  "status": 409,
  "detail": "Idempotency-Key was used with a different request",
  "instance": "<the X-Request-ID of this request>",
  "code": "idempotency_conflict"
}

Вебхуки

Зарегистрируйте эндпоинт HTTPS для каждого аккаунта или один общий для всей организации. Каждая доставка подписывается секретом эндпоинта, отображаемым при его создании или ротации.

  • Fax-Timestamp: секунды Unix.
  • Fax-Signature: hex(hmac_sha256(secret, "{timestamp}.{raw_body}")).
  • Отклоняйте метки времени с расхождением более пяти минут и игнорируйте повторы доставок (сверяйте delivery_id), сохраняющийся при повторах.
  • Повторные попытки выполняются через 1 минуту, 5 минут, 30 минут, 2 часа и 8 часов.
  • Вебхук никогда не передает содержимое документа; запрашивайте fax.links.document с вашим API-ключом.
  • fax.queued
  • fax.succeeded
  • fax.failed
  • fax.received
  • fax.rejected
  • fax.cancelled
  • fax.expired
Пример доставки
POST /your/webhook HTTP/1.1
Content-Type: application/json
Fax-Timestamp: 1793110800
Fax-Signature: 5b0e6c...e91d

{
  "event": "fax.received",
  "delivery_id": "0b6f3c7e-2a91-4d5b-8e40-1c7a9f2d6e38",
  "occurred_at": "2026-10-27T14:20:00Z",
  "fax": { "id": "...", "status": "succeeded", "direction": "inbound", "pages": 3,
           "links": { "self": "...", "attempts": "...", "document": "https://api.faxdev.com/v1/accounts/.../faxes/.../document" } }
}
Язык проверки подписи
Проверка вебхука · Node.js
// Node 18+ / Express: verify before you parse. Use the raw body bytes.
import crypto from "node:crypto";

export function verifyFaxWebhook(rawBody /* Buffer */, headers, secret /* "whsec_..." */) {
  const ts = headers["fax-timestamp"];
  const sigHeader = headers["fax-signature"];
  if (typeof ts !== "string" || typeof sigHeader !== "string" || !ts || !sigHeader) return false;
  if (!/^[0-9]+$/.test(ts)) return false;                                 // reject a non-numeric timestamp
  const tsUnix = Number(ts);
  if (!Number.isSafeInteger(tsUnix) || Math.abs(Date.now() / 1000 - tsUnix) > 300) return false;
  const expected = crypto.createHmac("sha256", secret)
    .update(`${ts}.`).update(rawBody).digest("hex");
  // After a secret rotation the header carries two comma-separated signatures for one hour.
  return sigHeader.split(",").some((candidate) => {
    const sig = candidate.trim();
    if (sig.length !== expected.length || !/^[0-9a-f]+$/.test(sig)) return false;
    const a = Buffer.from(sig, "hex"), b = Buffer.from(expected, "hex");
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  });
}
Проверка вебхука · Python
# Python 3: same check. Cache f"{ts}.{signature}" for 5 minutes to drop replays.
import hashlib, hmac, time

def verify_fax_webhook(raw_body: bytes, headers: dict, secret: str) -> bool:
    h = {k.lower(): v for k, v in headers.items()}
    ts, sig_header = h.get("fax-timestamp"), h.get("fax-signature")
    if not isinstance(ts, str) or not isinstance(sig_header, str) or not ts or not sig_header:
        return False
    try:
        ts_unix = int(ts)
    except ValueError:
        return False
    if abs(time.time() - ts_unix) > 300:  # reject skew over 5 minutes
        return False
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    # After a secret rotation the header carries two comma-separated signatures for one hour.
    for candidate in sig_header.split(","):
        sig = candidate.strip()
        if len(sig) != len(expected) or any(c not in "0123456789abcdef" for c in sig):
            continue
        if hmac.compare_digest(expected, sig):
            return True
    return False

Используйте тестовое действие эндпоинта, чтобы получить подписанное событие webhook.test до отправки первого реального факса.

MCP для ИИ-агентов

Интерфейс FaxDev API также работает как сервер Model Context Protocol (Streamable HTTP) по адресу https://api.faxdev.com/mcp. Он использует тот же bearer API-ключ, что и REST. Список инструментов генерируется из API и фильтруется по уровню и скоупам ключа, поэтому агент не видит недоступные инструменты.

Настройка MCP-клиента
mcp.json
{
  "mcpServers": {
    "fax": {
      "type": "http",
      "url": "https://api.faxdev.com/mcp",
      "headers": { "Authorization": "Bearer bfx_live_..." }
    }
  }
}
Параметры подключения · используйте команду настройки вашего клиента
# In your MCP client's server setup, enter:
Name: fax
Transport: Streamable HTTP
URL: https://api.faxdev.com/mcp
Header: Authorization: Bearer bfx_live_...

# CLI syntax varies by client. Use these values
# with your client's add-server command.

Выдавайте агентам ключи только с необходимыми скоупами. Агент для чтения не должен иметь скоуп faxes:send.

. Инструменты, доступные ключу аккаунта при наличии скоупов:

MCP-инструменты уровня аккаунта и соответствующие REST-операции
ИнструментОперация RESTСкоуп
send_faxPOST /v1/accounts/{account_id}/faxesfaxes:send
get_faxGET /v1/accounts/{account_id}/faxes/{fax_id}faxes:read
list_faxesGET /v1/accounts/{account_id}/faxesfaxes:read
cancel_faxPOST /v1/accounts/{account_id}/faxes/{fax_id}/cancelfaxes:cancel
list_attemptsGET …/faxes/{fax_id}/attemptsfaxes:read
get_fax_document_urlGET …/faxes/{fax_id}/document (returns a 5-minute URL)faxes:read
list_numbersGET /v1/accounts/{account_id}/numbersnumbers:read
list_contactsGET /v1/accounts/{account_id}/contactscontacts:read
create_contactPOST /v1/accounts/{account_id}/contactscontacts:write
get_usageGET /v1/accounts/{account_id}/usageusage:read

get_fax_document_url возвращает короткоживущий URL вместо содержимого файла, чтобы логи агента не засорялись документами; каждый вызов протоколируется в аудите. send_fax принимает content_ref из предварительной загрузки (рекомендуется) или данные base64 объемом до 20 МиБ.

Полный справочник: Документация API . Вопросы: свяжитесь с нами.

Доставка и надежность

Мы гарантируем доставку. Каждый исходящий факс маршрутизируется через несколько независимых операторов с контролем качества и коррекцией ошибок. Занятые линии, неотвеченные вызовы и сбои соединения обрабатываются автоматически. Если основные маршруты недоступны, подключается резервная сеть оператора (при этом номер отправителя может измениться). Повторно отправлять факс не требуется, а оплата взимается только за доставленные страницы.

  • Журнал каждой попытки доставки. Каждая попытка отображается в links.attempts с результатом, переданными страницами и длительностью.
  • Идемпотентный API. Повторите запрос создания с тем же Idempotency-Key и получите исходный факс — дубликат никогда не будет создан.
  • Подписанные вебхуки. Подпись HMAC-SHA256 по метке времени и телу запроса с повторами по графику. Игнорируйте дубликаты доставок (сверяйте delivery_id).
  • Шифрование при хранении. Документы и резервные копии зашифрованы, а каждое чтение фиксируется в журнале аудита.
  • Емкость по требованию. Пропускная способность масштабируется по регионам США; узлы плавно выводятся из работы, давая завершиться текущим факсам.

Вопросы разработчиков

Как пройти аутентификацию?

Передайте API-ключ в заголовке Authorization: Bearer. Ключи имеют префикс bfx_live_ или bfx_test_, содержат только выданные скоупы и могут быть отозваны в любой момент.

Как избежать повторной отправки одного факса?

Передавайте заголовок Idempotency-Key при каждом создании. Повторный запрос с тем же ключом в течение 24 часов возвращает исходный ответ без создания второго факса.

Как проверить подпись вебхука?

Вычислите hex(hmac_sha256(secret, timestamp + '.' + raw_body)) и сравните за постоянное время с заголовком Fax-Signature. Отклоняйте метки времени с расхождением более пяти минут и игнорируйте дубликаты доставок (сверяйте delivery_id).

Как ИИ-агент подключается через MCP?

Укажите MCP-клиенту адрес https://api.faxdev.com/mcp по Streamable HTTP с тем же bearer API-ключом, что и для REST. Список инструментов генерируется из API и фильтруется по скоупам ключа.

Можно ли протестировать без набора номера и оплаты?

Да. Ключ bfx_test_ создает тестовые факсы: они рендерятся и проверяются как реальные, затем помечаются succeeded без набора номера и списаний.

Что происходит при неудачной попытке доставки?

Вам ничего не нужно делать. Занятая линия, отсутствие ответа или сбой рукопожатия автоматически обрабатываются повторами и сменой маршрута через независимых операторов. Если основные маршруты недоступны, используется альтернативная сеть. В этом случае caller_id_delivery принимает значение not_guaranteed: страницы доставлены, но получатель может увидеть другой номер отправителя. Каждая попытка отражена в links.attempts, а недоставленные страницы не тарифицируются.