Desenvolvedores

Uma API de fax que se comporta como o restante da sua stack.

REST sobre HTTPS com JSON, chaves de API bearer, webhooks assinados e um servidor MCP gerado a partir da mesma API. Um reenvio nunca cria um segundo fax.

API HTTP

REST e JSON com chaves bearer. Faça o upload, envie com um Idempotency-Key e depois consulte o status ou aguarde o webhook.

POST /v1/accounts/{id}/faxes

Servidor MCP

A mesma API como ferramentas do Model Context Protocol, filtrada pelos escopos da sua chave, para agentes que utilizam fax.

send_fax · get_fax · list_faxes

Webhooks

Entregas assinadas com HMAC-SHA256 contendo o mesmo objeto de fax, com novas tentativas programadas. Ignore repetições da mesma entrega (compare por delivery_id).

fax.received · fax.succeeded

Início rápido

Crie uma chave de API no aplicativo na seção de chaves da sua conta. As chaves começam com bfx_live_… ou bfx_test_…. Uma chave de teste cria faxes no sandbox: renderizados e verificados como os reais, e depois marcados como succeeded sem discagem nem cobrança. Envie a chave como Authorization: Bearer.

  1. 1. UploadPDF, TIFF, JPEG ou PNG, até 20 MiB. O content_ref dura uma hora e é de uso único.
  2. 2. Enviarto está no formato E.164. Reutilize o mesmo Idempotency-Key em novas tentativas durante 24 horas.
  3. 3. AcompanharConsulte o status do fax ou aguarde fax.succeeded ou fax.failed.
Linguagem do início rápido
Upload, envio e acompanhamento · 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"
Upload, envio e consulta · 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"])
Upload, envio e consulta · 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);

Assim que o documento existir, links.document redireciona (302) sua requisição autenticada para uma URL de download válida por cinco minutos. Em contas com registro de acesso a documentos, a mesma requisição retorna o documento diretamente (200), e ?redirect=false retorna um link de uso único que funciona apenas uma vez, durante 60 segundos. Cada leitura de documento é registrada em log de auditoria com quem leu e como. Se o fax for concluído por uma rede alternativa de operadora, caller_id_delivery fica como "not_guaranteed": as páginas foram entregues, mas o destinatário pode visualizar um identificador de chamada diferente.

Respostas

Um fax passa por queued → rendering → admitted → … e termina como succeeded, failed, cancelled, expiredou rejected. O webhook traz o mesmo objeto. Entenda cada status pelo que ele representa:

  • queued, rendering: aceito e sendo convertido em páginas de fax.
  • admitted: verificado e agendado para entrega.
  • transmitting: as páginas estão sendo transmitidas agora.
  • retrying: uma tentativa não foi concluída; já agendamos a próxima, possivelmente por outra rede de operadora. Você não precisa reenviar.
  • succeeded: entregue e faturado pelas páginas que chegaram. failed, cancelled, expirede rejected não são cobrados.

Um fax pode passar por outros status intermediários enquanto a conexão é estabelecida. Considere qualquer status não terminal como em andamento, e o histórico detalhado em links.attempts como o relatório completo.

Exemplos de resposta
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"
}

Erros e novas tentativas

Erros retornam como application/problem+json com um identificador estável code. Repita erros de rede e 429 / 5xx com o mesmo Idempotency-Key, e respeite o cabeçalho Retry-After. Não repita outros status 4xx sem alterar a requisição.

Documento de problema
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"
}

Webhooks

Cadastre um endpoint HTTPS por conta ou um por organização para todas as contas de clientes. Cada envio é assinado com o segredo do endpoint exibido na criação ou rotação.

  • Fax-Timestamp: segundos Unix.
  • Fax-Signature: hex(hmac_sha256(secret, "{timestamp}.{raw_body}")).
  • Rejeite timestamps com mais de cinco minutos de diferença do seu relógio e ignore repetições da mesma entrega (verifique por delivery_id), que permanece o mesmo entre tentativas.
  • Tentamos novamente após 1 minuto, 5 minutos, 30 minutos, 2 horas e 8 horas.
  • Um webhook nunca transmite os bytes do documento; obtenha fax.links.document com sua chave de API.
  • fax.queued
  • fax.succeeded
  • fax.failed
  • fax.received
  • fax.rejected
  • fax.cancelled
  • fax.expired
Exemplo de entrega
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" } }
}
Linguagem de verificação de assinatura
Verificar um webhook · 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);
  });
}
Verificar um webhook · 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

Use a ação de teste do endpoint para receber um evento assinado webhook.test antes do seu primeiro fax real.

MCP para agentes de IA

A FaxDev API também funciona como um servidor Model Context Protocol (Streamable HTTP) em https://api.faxdev.com/mcp. Ela utiliza a mesma chave de API bearer do REST. A lista de ferramentas é gerada pela API e filtrada pelo nível e escopos da sua chave, garantindo que o agente nunca veja ferramentas que não pode chamar.

Configuração do cliente MCP
mcp.json
{
  "mcpServers": {
    "fax": {
      "type": "http",
      "url": "https://api.faxdev.com/mcp",
      "headers": { "Authorization": "Bearer bfx_live_..." }
    }
  }
}
Configurações de conexão · use o comando de setup do seu cliente
# 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.

Conceda aos agentes uma chave apenas com os escopos necessários. Um agente somente leitura nunca deve ter faxes:send.

Ferramentas disponíveis para uma chave de conta, de acordo com os escopos:

Ferramentas MCP de nível de conta e a respectiva operação REST executada
FerramentaOperação RESTEscopo
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 retorna uma URL temporária em vez dos bytes do arquivo, evitando encher transcrições de agentes com documentos; cada chamada é registrada em auditoria como um download no aplicativo. send_fax aceita um content_ref de um upload prévio (recomendado) ou conteúdo em base64 de até 20 MiB.

Referência completa: Documentação da API . Dúvidas: fale conosco.

Entrega e confiabilidade

Nós garantimos a entrega. Cada fax de saída é roteado por múltiplas redes de operadoras independentes com transporte e correção de erro dedicados para fax. Linhas ocupadas, não atendimento e falhas de handshake são repetidos e redirecionados automaticamente; se as rotas principais falharem, uma rede alternativa assume, podendo exibir outro identificador de chamada. Você nunca precisa reenviar e só paga pelas páginas que chegarem.

  • Registro de entrega por tentativa. Cada tentativa é listada em links.attempts com seu resultado, páginas transferidas e duração.
  • API idempotente. Repita a criação com a mesma Idempotency-Key e você receberá o fax original, nunca um segundo.
  • Webhooks assinados. HMAC-SHA256 sobre o timestamp e o corpo, com novas tentativas programadas. Ignore repetições da mesma entrega (verifique por delivery_id).
  • Criptografia em repouso. Documentos e backups são criptografados, e cada leitura é auditada.
  • Capacidade que acompanha a demanda. A capacidade de entrega escala automaticamente entre regiões dos EUA; um nó passa por drenagem antes de ser desligado para que os faxes em andamento terminem primeiro.

Dúvidas de desenvolvedores

Como faço para me autenticar?

Envie uma chave de API como Authorization: Bearer. As chaves começam com bfx_live_ ou bfx_test_, contêm apenas os escopos concedidos e podem ser revogadas a qualquer momento.

Como evito enviar o mesmo fax duas vezes?

Envie o cabeçalho Idempotency-Key em cada criação. Repetir a requisição com a mesma chave dentro de 24 horas retorna a resposta original em vez de enviar um segundo fax.

Como verifico um webhook?

Calcule hex(hmac_sha256(secret, timestamp + '.' + raw_body)) e compare em tempo constante com o cabeçalho Fax-Signature. Rejeite timestamps com diferença superior a cinco minutos do seu relógio e ignore repetições da mesma entrega (compare por delivery_id).

Como um agente de IA se conecta via MCP?

Aponte um cliente MCP para https://api.faxdev.com/mcp via Streamable HTTP usando a mesma chave de API bearer do REST. A lista de ferramentas é gerada a partir da API e filtrada pelos escopos da chave.

Posso testar sem discar ou pagar?

Sim. Uma chave bfx_test_ cria faxes no sandbox que são renderizados e validados como os reais, sendo depois marcados como succeeded sem discagem nem cobrança.

O que acontece quando uma tentativa de entrega falha?

Você não precisa fazer nada. Linha ocupada, ausência de resposta ou falha de handshake são repetidos e redirecionados automaticamente entre redes de operadoras independentes. Se as rotas principais falharem, uma rede alternativa é utilizada. Quando isso acontece, caller_id_delivery fica como not_guaranteed: as páginas foram entregues, mas o destinatário pode ver outro identificador de chamada. Cada tentativa é listada em links.attempts, e páginas que falharam nunca são cobradas.