Os 12 Erros Mais Comuns ao Integrar uma API de Assinatura (e Como Corrigir)
Depois de acompanhar centenas de integrações no sandbox, os mesmos tropeços aparecem em ordem quase previsível — do /oauth/token sem o "2" na primeira hora ao redirect tratado como prova na primeira semana de produção. Este guia lista os 12 erros mais comuns, cada um com sintoma, causa e correção — para você reconhecer o seu em segundos, não em tardes.
Autenticação (os da primeira hora)
1. /oauth/token sem o "2" — ou com corpo JSON
Sintoma: 404 ou erro de requisição no primeiro POST.
Causa: o endpoint é /oauth2/token, e o corpo vai em application/x-www-form-urlencoded — não em JSON. Quem vem de APIs que aceitam JSON no token tropeça aqui.
Correção: -d "grant_type=client_credentials" -d "client_id=..." -d "client_secret=...". O fluxo completo está em OAuth2 e autenticação.
2. Cachear o token por 1 hora
Sintoma: tudo funciona por 15 minutos, depois chove 401.
Causa: o expires_in é 900 (15 minutos), não 3600 — e há código por aí com 3600 hardcoded.
Correção: respeite o expires_in da resposta e renove com folga — ou use um SDK oficial, que faz isso sozinho.
3. Credencial no lugar errado
Sintoma: funciona — e é exatamente esse o problema.
Causa: client_secret em código de front-end, variável NEXT_PUBLIC_, custom action de app móvel ou repositório público.
Correção: credencial da conta só existe no servidor. O que pode ir ao cliente é o clientSecret da sessão — segredo de escopo único, não da conta.
Criação da sessão (os do primeiro dia)
4. DIGITAL_SIGN_A1 como profile
Sintoma: 400 na criação, "profile inválido".
Causa: DIGITAL_SIGN_A1 é um tipo de etapa que aparece na resposta — o perfil de certificado ICP-Brasil chama-se DIGITAL_CERTIFICATE.
Correção: perfis válidos: CLICK_ONLY, CLICK_PLUS_OTP, BIOMETRIC, BIOMETRIC_PLUS_OTP, DIGITAL_CERTIFICATE e variantes SERPRO. O mapa perfil→etapas está em autenticação multimétodo.
5. Payload com nomes de outra API
Sintoma: 400 com campos "não reconhecidos".
Causa: document.content_base64, redirect_url, signers[] na sessão — vocabulário de outras plataformas (ou de tutoriais desatualizados).
Correção: a forma real: purpose, policy.profile, signer (singular na Expressa), document.content + document.filename, returnUrl. Multi-signatário é envelope + uma sessão por signatário.
6. PDF acima de 10 MB inline
Sintoma: erro na criação com documentos escaneados grandes.
Causa: o limite do documento inline em base64 é 10 MB — e o base64 infla o arquivo em ~33%.
Correção: comprima o PDF ou use o fluxo de presigned URL (criar sessão sem documento → presign → PUT → confirm). E gere a presigned URL na hora do upload: ela expira em 10 minutos.
Entrega do link (os da primeira demo)
7. Enviar a url sem o ?cs=
Sintoma: o signatário abre o link e a página não autoriza a sessão.
Causa: o link final é url + "?cs=" + clientSecret — os dois campos da resposta, combinados.
Correção: monte o link na resposta da criação e trate-o como credencial: é ele que autoriza o acesso à sessão.
8. Link de CLICK_ONLY em canal aberto
Sintoma: nenhum — até alguém contestar quem clicou.
Causa: sem OTP nem biometria, o link é a autenticação: quem o tem, conclui o aceite.
Correção: entregue por canal autenticado (sessão logada do app, e-mail verificado). Sem canal confiável? Suba para CLICK_PLUS_OTP. A regra completa está em aceite de termos via API.
Webhooks (os da primeira semana)
9. Verificar o HMAC sobre o JSON re-serializado
Sintoma: 100% dos webhooks falham na verificação, "mas o segredo está certo".
Causa: a assinatura cobre o corpo bruto; parsear e re-serializar muda os bytes.
Correção: leia o corpo cru (request.text(), raw body, php://input), verifique, depois parseie. Falha intermitente? Relógio do servidor — a tolerância anti-replay é de 5 minutos.
10. Tratar reentrega como evento novo
Sintoma: e-mails duplicados, status processado duas vezes.
Causa: retry com backoff significa que o mesmo evento pode chegar mais de uma vez — é o contrato de entrega, não um bug.
Correção: idempotência pelo id do evento: processou, registrou, repetiu → 200 sem efeito. O receptor de referência está em webhooks e eventos.
11. Redirect como prova de assinatura
Sintoma: documentos "assinados" no seu sistema que ninguém assinou.
Causa: o usuário voltar pelo returnUrl não prova conclusão — ele pode ter voltado sem assinar (ou nunca voltar, tendo assinado).
Correção: o estado verdadeiro vem do webhook SIGNING_SESSION.COMPLETED ou de GET /v1/transactions/{id}. Redirect é UX; webhook é fato.
Sandbox (o da segunda semana)
12. Depender de entidades antigas de homologação
Sintoma: 404 em transações que "existiam semana passada"; testes que quebram sozinhos.
Causa: TTL de 7 dias — entidades de HML expiram por desenho.
Correção: todo teste cria o que consome; nenhum ID fixo de execuções passadas. A estratégia completa está em como testar do sandbox ao CI. (E confira o host: é api-hml.signdocs.com.br, com hífen — não api.hml.)
code legível) estão catalogados em códigos de erro e falhas.
Perguntas Frequentes
Meu POST no /oauth/token retorna erro. O que está errado?
Dois clássicos, geralmente juntos. Primeiro: o endpoint é /oauth2/token — com o 2. Segundo: o corpo vai em application/x-www-form-urlencoded (grant_type=client_credentials&client_id=...&client_secret=...), não em JSON. Quem vem de APIs que aceitam JSON no token endpoint tropeça aqui na primeira hora. Bônus do mesmo tema: o expires_in é 900 (15 minutos), não 3600 — se o seu código cacheia o token por uma hora, as chamadas começam a falhar com 401 depois de 15 minutos.
O signatário abre o link e a página diz que a sessão é inválida. Por quê?
Quase sempre é o link incompleto: a resposta da criação traz url e clientSecret como campos separados, e o link que o signatário abre é a combinação dos dois — url?cs=clientSecret. Quem envia só a url manda o signatário a uma página que não pode autorizar. A segunda causa é expiração: a sessão tem prazo (e no sandbox, TTL de 7 dias) — confira o status em GET /v1/transactions/{id} antes de reencaminhar um link antigo.
Recebo 400 ao criar a sessão com DIGITAL_SIGN_A1. Não é o perfil de certificado?
Não — DIGITAL_SIGN_A1 é um tipo de etapa (step.type) que aparece na resposta da transação, nunca um valor de policy.profile. O perfil que exige certificado ICP-Brasil é DIGITAL_CERTIFICATE; internamente ele vira as etapas CLICK_ACCEPT e DIGITAL_SIGN_A1. Enviar o nome da etapa como perfil é o 400 mais frequente da API. Os perfis válidos: CLICK_ONLY, CLICK_PLUS_OTP, BIOMETRIC, BIOMETRIC_PLUS_OTP, DIGITAL_CERTIFICATE e as variantes SERPRO — um por signatário.
Meu webhook sempre falha na verificação de assinatura. O código parece certo.
O HMAC-SHA256 é calculado sobre o corpo bruto da requisição — a sequência exata de bytes recebida. O erro clássico é deixar o framework parsear o JSON e depois re-serializar para verificar: a ordem das chaves ou o espaçamento muda, os bytes mudam, a assinatura nunca bate. Leia o corpo cru (request.text(), raw body, php://input), verifique o HMAC sobre ele e só então faça o parse. Se estiver tudo certo e falhar só às vezes, confira o relógio do servidor: a tolerância anti-replay do timestamp é de 5 minutos.
Reenviei uma requisição com a mesma X-Idempotency-Key e recebi 409. Não era para deduplicar?
A idempotência deduplica repetições exatas: mesma chave + mesmo corpo devolvem a resposta original. Mesma chave com corpo diferente é conflito — 409 de propósito, porque reutilizar chave para payloads distintos quase sempre é bug (um contador que não incrementou, um ID de pedido reaproveitado). A correção é gerar a chave a partir do que torna a operação única no seu domínio: o ID do pedido, do contrato, do usuário+versão dos termos. E lembre que a janela é de 24 horas — depois disso, a mesma chave cria uma operação nova.
Minha integração funcionava no sandbox e as entidades sumiram. Fui bloqueado?
Não — é o TTL: entidades de homologação (sessões, envelopes, transações, evidências) expiram automaticamente em 7 dias. É desenho, não incidente: mantém o sandbox limpo e reforça que homologação não é armazenamento. Se seus testes referenciam IDs fixos de execuções antigas, eles quebram sozinhos — todo teste deve criar o que consome. Os 404 em entidades antigas de HML são o sintoma clássico. Em produção a retenção é durável, conforme contrato.
Erre no sandbox, não em produção
Credenciais de homologação gratuitas, erros estruturados com código legível e webhook de teste sob demanda — os 12 tropeços acima se resolvem numa tarde de sandbox.
Criar credenciais de homologação Falar com o time comercial