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.
| Escopo | Dá acesso a |
|---|---|
| documents:read | listar/detalhar documentos, URLs temporárias, download/certificado |
| documents:write | criar documento, anexar PDF, enviar, cancelar, excluir |
| documents:sign | configurar signatário-emissor (assinatura server-side em escala) |
| signers:write | adicionar/editar/remover signatários, reenviar convite |
| signers:link | obter o link de assinatura para entregar pelo seu próprio canal (app, QR code) |
| fields:write | campos posicionados (carimbo visual) |
| audit:read | trilha de auditoria do documento |
| templates:read / templates:write | templates de documento: listar, criar, editar, materializar |
| webhooks:read / webhooks:write | gerenciar webhooks e entregas |
| billing:read | consultar saldo da carteira (somente leitura) |
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
# 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 só 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:
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".
# 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 4
Entregar o link pelo seu canal (app, QR code)
Se você já tem app ou portal próprio, pode pegar o link de assinatura e entregá-lo você mesmo — renderizar um QR code na tela, mandar pelo seu SMS transacional, embutir no seu fluxo. A resposta é síncrona: a URL vem na hora, sem esperar webhook.
# Entregar o link VOCÊ MESMO (app próprio, QR code na tela, seu SMS).
# Devolve a URL na hora — nada de esperar webhook. A AEDOC NÃO envia
# e-mail/WhatsApp para este signatário: a entrega passa a ser sua.
# Dispensa o /send: se o envelope ainda estiver em preparo, esta chamada
# já o coloca em circulação (e consome 1 do limite do plano).
curl -s -X POST "https://api.aedoc.com.br/v1/documents/$DOC/signers/$SIGNER/signing-link" \
-H "Authorization: Bearer $AEDOC_KEY"
# Resposta (a URL sai UMA vez — guarde; nem nós conseguimos relê-la):
# {"data": {
# "url": "https://app.aedoc.com.br/assinar/9f8a…",
# "expires_at": "2026-08-16T12:00:00-03:00",
# "signer_id": "01kwe…",
# "delivery": "integrator"
# }}
# Regras que valem a pena saber ANTES de integrar:
# - exige 2º fator no signatário (otp_email, otp_sms ou whatsapp). Sem ele
# a assinatura seria SIMPLES, não avançada, então recusamos com 422.
# - o código OTP continua indo direto da AEDOC para o signatário. Ele é a
# prova de autoria: não é repassado ao integrador, por nenhuma via.
# - chamar de novo gera um link ADICIONAL; o anterior CONTINUA valendo
# (seu QR já impresso não quebra). Para REVOGAR, use .../resend — que
# exige envelope em circulação e dispara convite novo pela AEDOC.
# - envelope PARALELO ainda não enviado: esta chamada convida os DEMAIS
# signatários por e-mail/WhatsApp na mesma hora. Para um envelope 100%
# entregue por você, use signing_type "sequential".
# - o certificado registra "Link entregue pelo emissor" — não afirmamos
# um canal de entrega que não foi nosso.
# - custa R$ 0,05 por emissão (plano com medição). Sem saldo, respondemos
# 422 em vez de emitir a descoberto — trate esse caso.- Exige o escopo
signers:link, separado designers:writede propósito: o link é uma credencial portadora — quem o tem assina como aquele signatário. - Nós não notificamos este signatário: nem e-mail, nem WhatsApp — a entrega é sua.
- Atenção ao envelope paralelo: se ele ainda não tinha sido enviado, esta chamada o coloca em circulação e convida os demais signatários pelo caminho normal (e-mail/WhatsApp) na mesma hora. Chamar o endpoint de novo para os outros não evita isso — quando a segunda chamada chega, os convites já saíram. Para um envelope inteiramente entregue por você, use ordem
sequential: aí cada signatário só é convidado quando chega a vez dele, e você chama o endpoint a cada rodada. - Exige 2º fator no signatário (
otp_email,otp_smsouwhatsapp). Sem ele a assinatura seria simples, não avançada — então recusamos com422em vez de entregar evidência mais fraca sem avisar. - O código OTP continua saindo da AEDOC direto para o signatário e nunca é repassado ao integrador. É ele que prova a autoria: se o emissor pudesse obtê-lo, não haveria como distinguir a assinatura da pessoa da assinatura da empresa.
- Chamar de novo gera um link adicional; o anterior continua valendo até expirar, para não quebrar um QR já distribuído.
- Para revogar um link vazado, use
.../signers/{id}/resend, que invalida todos os links pendentes do signatário. Ele exige o envelope em circulação e o signatário já convidado — e dispara um convite novo pela AEDOC (e-mail/WhatsApp), o que é o preço de revogar. Signatário que ainda aguarda a vez na ordem sequencial não tem link ativo para revogar, e a chamada responde422. - O certificado registra “Link entregue pelo emissor” — não afirmamos um canal de entrega que não foi nosso.
Passo 5
Receber webhooks
Cadastre um endpoint https público e os eventos que interessam:
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.remindedO corpo é enxuto de propósito (LGPD): ids e links, nunca nome, e-mail ou CPF. Busque os dados pela API com a sua chave.
{
"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:
- Calcule sobre o corpo cru recebido (bytes exatos), nunca sobre o JSON re-serializado pelo seu framework — reordenar chaves quebra o HMAC.
- Compare em tempo constante (
timingSafeEqual/hash_equals). - Rejeite
tfora 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.
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);
});$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:
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ção | Preço | Regras |
|---|---|---|
| Consulta à API (Bearer) | R$ 0,0008 | só requests SERVIDOS contam — 429 do rate limit e 5xx nossos NÃO são cobrados; GET /v1/wallet é isento |
| Criação de documento via API | R$ 0,05 | POST /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,05 | POST .../signers/{id}/signing-link, por emissão. Sem saldo, a chamada é recusada com 422 (não emitimos link a descoberto) |
| Webhook entregue | R$ 0,0001 | só entrega com resposta 2xx conta — retries de endpoint fora do ar não custam |
| Convite de assinatura por WhatsApp | R$ 0,10 | por envio bem-sucedido; sem saldo, o convite segue só por e-mail |
| OTP por WhatsApp ou SMS | R$ 0,15 | por 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ódigo | Significado |
|---|---|
| 401 | chave ausente, inválida ou revogada (resposta genérica de propósito) |
| 403 insufficient_scope | a chave não tem o escopo da operação |
| 404 | recurso inexistente ou de outro tenant (indistinguíveis, por segurança) |
| 409 | estado incompatível (ex.: download antes de signed) |
| 422 | validação (corpo {message, errors}) — inclui limite do plano no /send |
| 429 rate_limited | rate 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 comGET /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-secretsem 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.
