Guia de integração

Este guia leva você do zero ao primeiro documento assinado via API, com webhooks entregando os eventos do ciclo de vida. O contrato completo — todas as rotas, schemas e códigos de erro — é a referência da API; esta página é o caminho feliz comentado.

Passo 1

Criar uma chave de API

No painel: API & Webhooks → Nova chave. Escolha os escopos mínimos que a sua integração precisa — a chave aparece uma única vez.

EscopoDá acesso a
documents:readlistar/detalhar documentos, URLs temporárias, download/certificado
documents:writecriar documento, anexar PDF, enviar, cancelar, excluir
documents:signconfigurar signatário-emissor (assinatura server-side em escala)
signers:writeadicionar/editar/remover signatários, reenviar convite
signers:linkobter o link de assinatura para entregar pelo seu próprio canal (app, QR code)
fields:writecampos posicionados (carimbo visual)
audit:readtrilha de auditoria do documento
templates:read / templates:writetemplates de documento: listar, criar, editar, materializar
webhooks:read / webhooks:writegerenciar webhooks e entregas
billing:readconsultar saldo da carteira (somente leitura)
bash
export AEDOC_KEY="aedoc_live_…"

Nunca versione a chave nem a exponha no front-end: ela fala pelo seu tenant inteiro.

Passo 2

Primeiro documento assinado

bash
# 1. Criar o envelope (Idempotency-Key é opt-in, recomendado)
DOC=$(curl -s -X POST "https://api.aedoc.com.br/v1/documents" \
  -H "Authorization: Bearer $AEDOC_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-8812-contrato" \
  -d '{"title": "Contrato de prestação de serviços"}' | jq -r '.data.id')

# 2. Anexar o PDF (só PDF; rejeitamos JavaScript embutido, cifrados etc.)
curl -s -X POST "https://api.aedoc.com.br/v1/documents/$DOC/files" \
  -H "Authorization: Bearer $AEDOC_KEY" \
  -F "file=@contrato.pdf"

# 3. Adicionar o signatário (auth_method default: otp_email — o gate da
#    assinatura AVANÇADA; "email" puro é escolha explícita, rotulada).
#    Só-WhatsApp? Mande "phone" SEM "email": convite e OTP saem pelo
#    telefone (exige convite por WhatsApp habilitado na conta).
curl -s -X POST "https://api.aedoc.com.br/v1/documents/$DOC/signers" \
  -H "Authorization: Bearer $AEDOC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Maria Silva", "email": "maria@exemplo.com.br"}'

# 4. Enviar (dispara o magic link por e-mail; consome 1 do limite do plano)
curl -s -X POST "https://api.aedoc.com.br/v1/documents/$DOC/send" \
  -H "Authorization: Bearer $AEDOC_KEY"

O signatário recebe o e-mail, abre a página pública de assinatura, valida a identidade (OTP por e-mail) e assina. Você não precisa — nem consegue — assinar por ele: a evidência é do dispositivo dele.

Convite por WhatsApp (quando ativo na sua conta): os contatos informados definem a entrega, sem campo de canal. Com email e phone (BR, com DDD), o convite sai pelos dois — o mesmo link. Com phone, o WhatsApp vira o canal do convite e do código OTP (auth_method assume whatsapp); nesse caso o telefone precisa ser válido e o envio é bloqueado com 422 se o canal estiver indisponível. Signatário sem e-mail fica fora da régua automática de lembretes — reenvie por POST …/signers/{id}/resend.

Quando o último signatário concluir, o status passa por processing e chega a signed. Só então:

bash
curl -s "https://api.aedoc.com.br/v1/documents/$DOC/download" \
  -H "Authorization: Bearer $AEDOC_KEY"      # → {"url": "…"} TTL 5 min
curl -s "https://api.aedoc.com.br/v1/documents/$DOC/certificate" \
  -H "Authorization: Bearer $AEDOC_KEY"      # certificado de auditoria (PDF)

Passo 3

Assinatura em escala (pré-assinatura do emissor)

Para casos de volume — contrato de adesão, folha, RH: centenas de envelopes/dia em que só o destinatário assina de fato — a sua empresa pode entrar como parte já assinada no disparo. Cadastre um signatário com "auth_method": "api".

bash
# Signatário-emissor: a empresa entra como parte já assinada no disparo
curl -s -X POST "https://api.aedoc.com.br/v1/documents/$DOC/signers" \
  -H "Authorization: Bearer $AEDOC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Acme Serviços LTDA", "email": "juridico@acme.com.br",
       "auth_method": "api", "sign_order": 1}'
  • Exige o escopo documents:sign — sem ele, 403 insufficient_scope. A autoridade de assinar em nome da empresa fica explícita na credencial.
  • Em envelope paralelo, o emissor sai assinado no /send; em sequencial, quando chega a vez dele.
  • O certificado rotula o método com honestidade: “Credencial autenticada do emissor — assinatura server-side (avançada)”.

Passo 5

Receber webhooks

Cadastre um endpoint https público e os eventos que interessam:

bash
curl -s -X POST https://api.aedoc.com.br/v1/webhooks \
  -H "Authorization: Bearer $AEDOC_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://app.suaempresa.com.br/hooks/aedoc",
    "events": ["document.signed", "document.declined", "document.expired", "document.failed"]
  }'

A resposta traz secret (whsec_…) uma única vez. Eventos disponíveis:

document.createddocument.uploadeddocument.sentdocument.viewedsigner.authenticateddocument.partially_signeddocument.signeddocument.declineddocument.expireddocument.cancelleddocument.failedsigner.reminded

O corpo é enxuto de propósito (LGPD): ids e links, nunca nome, e-mail ou CPF. Busque os dados pela API com a sua chave.

json
{
  "event": "document.signed",
  "event_id": "01kwf0evt0000000000000000",
  "occurred_at": "2026-07-03T15:04:05+00:00",
  "data": {
    "document_id": "01kwexy…",
    "links": {
      "document": "https://api.aedoc.com.br/v1/documents/01kwexy…",
      "audit": "https://api.aedoc.com.br/v1/documents/01kwexy…/audit"
    }
  }
}
  • Responda 2xx em menos de 10s (processe async se precisar). Qualquer outra coisa conta como falha.
  • Retry com backoff exponencial: ~8 tentativas ao longo de ~24h; depois a entrega fica exhausted.
  • Endpoint falhando por ~7 dias é auto-desativado (reative no painel).
  • Deduplique pelo event_id — ele é estável: toda reentrega chega com o mesmo id.

Passo 6

Verificar a assinatura HMAC (obrigatório)

Todo webhook chega com o header:

X-AEDOC-Signature: t=1751554245,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1 = HMAC-SHA256(secret, t + "." + corpo_cru). Três regras de ouro:

  1. Calcule sobre o corpo cru recebido (bytes exatos), nunca sobre o JSON re-serializado pelo seu framework — reordenar chaves quebra o HMAC.
  2. Compare em tempo constante (timingSafeEqual / hash_equals).
  3. Rejeite t fora de uma janela de ±5 minutos (anti-replay).

Durante uma rotação de segredo o header traz dois v1= por até 24h — aceite se qualquer um conferir com qualquer dos seus segredos.

Node.js — Express
const crypto = require("node:crypto");
const express = require("express");
const app = express();

const SECRET = process.env.AEDOC_WEBHOOK_SECRET; // whsec_…
const TOLERANCE_S = 300;

app.post("/hooks/aedoc", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.header("X-AEDOC-Signature") ?? "";
  const parts = header.split(",");
  const t = Number((parts.shift() ?? "").slice(2));          // t=<unix>
  const signatures = parts.map((p) => p.slice(3));           // v1=<hex>

  if (!t || Math.abs(Date.now() / 1000 - t) > TOLERANCE_S) {
    return res.status(400).send("timestamp fora da janela");
  }

  // Concatene BYTES, não strings: `${req.body}` decodifica o Buffer como UTF-8
  // e qualquer byte inválido vira U+FFFD — o HMAC sai errado em silêncio.
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(Buffer.concat([Buffer.from(`${t}.`), req.body]))  // req.body = Buffer cru
    .digest("hex");

  const valid = signatures.some((sig) => {
    const a = Buffer.from(sig, "hex");
    const b = Buffer.from(expected, "hex");
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  });
  if (!valid) return res.status(401).send("assinatura inválida");

  const event = JSON.parse(req.body);
  // Deduplique por event.event_id antes de processar!
  res.sendStatus(200);
});
PHP
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_AEDOC_SIGNATURE'] ?? '';

$parts = explode(',', $header);
$t = (int) substr(array_shift($parts), 2);
$signatures = array_map(fn ($p) => substr($p, 3), $parts);

abort_unless(abs(time() - $t) <= 300, 400);        // anti-replay

$expected = hash_hmac('sha256', $t.'.'.$raw, $secret);
// array_reduce (e não array_any, que exige PHP >= 8.4): roda em qualquer PHP suportado
$valid = array_reduce($signatures, fn ($ok, $sig) => $ok || hash_equals($expected, $sig), false);
abort_unless($valid, 401);

Passo 7

Saldo da carteira e preços por ação

Planos com carteira pré-paga (Professional) pagam add-ons e volume debitando do saldo. Para monitorar por máquina, crie a chave com o escopo billing:read:

bash
curl -s https://api.aedoc.com.br/v1/wallet \
  -H "Authorization: Bearer $AEDOC_KEY" | jq .data
# { balance_cents, balance_millicents, cap_cents, auto_recharge: {...}, ... }

Só leitura: recarga e auto-recarga são exclusivas do painel (decisão de gente, com 2FA). Plano sem carteira responde 404 wallet_unavailable.

AçãoPreçoRegras
Consulta à API (Bearer)R$ 0,0008só requests SERVIDOS contam — 429 do rate limit e 5xx nossos NÃO são cobrados; GET /v1/wallet é isento
Criação de documento via APIR$ 0,05POST /v1/documents e materialização de template com chave de API, só em 2xx; criado no painel NÃO conta
Link de assinatura entregue por vocêR$ 0,05POST .../signers/{id}/signing-link, por emissão. Sem saldo, a chamada é recusada com 422 (não emitimos link a descoberto)
Webhook entregueR$ 0,0001só entrega com resposta 2xx conta — retries de endpoint fora do ar não custam
Convite de assinatura por WhatsAppR$ 0,10por envio bem-sucedido; sem saldo, o convite segue só por e-mail
OTP por WhatsApp ou SMSR$ 0,15por envio; sem saldo, o código sai por e-mail (fallback automático — a assinatura nunca trava)

Documento criado no painel é sempre incluso no plano. O plano Free nunca é cobrado por ação — o teto de 30 documentos/mês é a trava.

Passo 8

Erros e boas práticas

CódigoSignificado
401chave ausente, inválida ou revogada (resposta genérica de propósito)
403 insufficient_scopea chave não tem o escopo da operação
404recurso inexistente ou de outro tenant (indistinguíveis, por segurança)
409estado incompatível (ex.: download antes de signed)
422validação (corpo {message, errors}) — inclui limite do plano no /send
429 rate_limitedrate limit do plano (Free 10 · Professional 60 · Enterprise 200 req/min, por tenant)
  • Trate o webhook como notificação, não como fonte de verdade: ao receber document.signed, confirme com GET /v1/documents/{id} antes de efeitos irreversíveis.
  • A verificação pública de integridade responde sempre íntegro (hash confere) | alterado | expirado | não encontrado — nunca “válido”. Reflita essa linguagem na sua UI.
  • Nunca logue a chave da API nem o segredo do webhook; rotacione por rotate-secret sem downtime (janela dupla de 24h).

Próximo passo: a referência completa da API — ou fale com a gente se algo aqui não resolveu.