Erros
A API responde erro no formato RFC 9457, com dezesseis códigos estáveis e um código de
correlação em toda resposta. É por esse código, o requestId, que um chamado de
suporte se resolve em minutos em vez de três dias de troca de e-mails.
>>> PENDENTE DE BACKEND <<<
Os dezesseis códigos e o formato da resposta vêm do contrato canônico
docs/CONTRATO_API.openapi.yaml. A correspondência entre código e status HTTP
indicada na tabela abaixo é a esperada, e será conferida contra o servidor quando ele
subir. Ramifique sempre pelo campo codigo, e não pelo status:
assim, um ajuste de status não quebra a sua integração.
Formato da resposta
Todo erro devolve um problem details da RFC 9457, com
Content-Type: application/problem+json. O campo que o seu código deve
ramificar é codigo, e não o status HTTP nem o texto.
{
"type": "https://docs.adigitalmax.com.br/erros.html#nao_encontrado",
"title": "Recurso não encontrado",
"status": 404,
"detail": "Recurso não encontrado.",
"instance": "/v1/documentos/6f2a1c94-8b3d-4e57-9a20-71c5f0d3ab48",
"codigo": "NAO_ENCONTRADO",
"requestId": "01J9M4X7QK2ZB8N3P6R0T5V2WY"
}
| Campo | Obrigatório | Para quê |
|---|---|---|
codigo | sim | É o que o seu código deve ramificar. Estável: um código nunca muda de nome dentro da mesma versão da API, e um status HTTP pode carregar mais de um código. |
type | sim | URI que aponta para a explicação. Aponta para esta página, na âncora do código. |
title | sim | Resumo curto e legível do tipo de problema. |
status | sim | O mesmo status HTTP da resposta, repetido no corpo para sobreviver a intermediários que o percam. |
detail | não | Texto sobre esta ocorrência. Não o mostre ao seu usuário final e não o use em condicional: ele é para quem lê o log. |
instance | não | O caminho que produziu o erro. |
requestId | não | O código de correlação. Registre sempre. |
erros | não | Presente apenas em erro de validação. Arranjo de objetos com campo e mensagem. |
A mensagem é deliberadamente pobre. Ela nunca revela nome de tabela,
consulta, rastro de pilha nem a razão exata de uma negação entre organizações. Isso não
é preguiça: qualquer variação de texto entre dois casos de negação vira um canal lateral
que permite deduzir, de fora, se um recurso existe. O detalhe completo do que aconteceu
fica no nosso log, ligado ao seu requestId, e o suporte alcança ele em
segundos.
O código de correlação
O requestId identifica uma requisição específica no nosso log. Registre-o em
todo log de chamada à nossa API, e não só quando der erro: a pergunta que mais atrasa
suporte é "o documento saiu duplicado ontem à tarde", e com o identificador da requisição
que criou cada um a resposta é imediata.
// Um cliente que registra o requestId em sucesso e em falha, e que
// distingue erro retentavel de erro definitivo. Copie inteiro.
const BASE = "https://api.adigitalmax.com.br/v1";
export class ErroApi extends Error {
constructor(status, corpo) {
super(`${corpo.codigo}: ${corpo.title}`);
this.name = "ErroApi";
this.status = status;
// Ramifique por codigo, e nunca por status nem por texto.
this.codigo = corpo.codigo;
this.requestId = corpo.requestId;
this.detail = corpo.detail;
this.erros = corpo.erros ?? [];
}
/** 429 e 5xx passam; o resto e defeito seu e retentar nao ajuda. */
get retentavel() {
return this.status === 429 || this.status >= 500;
}
}
const dormir = (ms) => new Promise((r) => setTimeout(r, ms));
export async function chamar(caminho, opcoes = {}, tentativa = 1) {
const resposta = await fetch(`${BASE}${caminho}`, {
...opcoes,
headers: {
Authorization: `Bearer ${process.env.ADM_CHAVE}`,
Accept: "application/json",
...opcoes.headers,
},
});
if (resposta.status === 204) return null;
const corpo = await resposta.json();
// Registre SEMPRE, e nao so no erro.
console.log(
JSON.stringify({
nivel: resposta.ok ? "info" : "erro",
rota: caminho,
status: resposta.status,
requestId: corpo.requestId ?? null,
tentativa,
})
);
if (resposta.ok) return corpo;
const erro = new ErroApi(resposta.status, corpo);
if (erro.retentavel && tentativa < 4) {
// Respeite o Retry-After quando ele vier; ele nao e sugestao.
const cabecalho = Number(resposta.headers.get("Retry-After"));
const espera = Number.isFinite(cabecalho) && cabecalho > 0
? cabecalho * 1000
: 2 ** tentativa * 500 + Math.random() * 300; // com jitter
await dormir(espera);
return chamar(caminho, opcoes, tentativa + 1);
}
throw erro;
}#!/usr/bin/env python3
"""Cliente que registra o requestId e retenta so o que vale retentar."""
import json
import logging
import os
import random
import time
import requests
BASE = "https://api.adigitalmax.com.br/v1"
log = logging.getLogger("adigitalmax")
class ErroApi(Exception):
def __init__(self, status: int, corpo: dict):
self.status = status
# Ramifique por codigo, e nunca por status nem por texto.
self.codigo = corpo.get("codigo")
self.request_id = corpo.get("requestId")
self.detail = corpo.get("detail")
self.erros = corpo.get("erros", [])
super().__init__(f"{self.codigo}: {corpo.get('title')}")
@property
def retentavel(self) -> bool:
"""429 e 5xx passam; o resto e defeito seu e retentar nao ajuda."""
return self.status == 429 or self.status >= 500
def chamar(metodo: str, caminho: str, tentativas: int = 4, **kwargs):
for tentativa in range(1, tentativas + 1):
resposta = requests.request(
metodo,
f"{BASE}{caminho}",
headers={
"Authorization": f"Bearer {os.environ['ADM_CHAVE']}",
"Accept": "application/json",
**kwargs.pop("headers", {}),
},
timeout=60,
**kwargs,
)
if resposta.status_code == 204:
return None
corpo = resposta.json()
# Registre SEMPRE, e nao so no erro.
log.info(json.dumps({
"rota": caminho,
"status": resposta.status_code,
"requestId": corpo.get("requestId"),
"tentativa": tentativa,
}))
if resposta.ok:
return corpo
erro = ErroApi(resposta.status_code, corpo)
if not erro.retentavel or tentativa == tentativas:
raise erro
# Retry-After nao e sugestao: obedeca quando vier.
cabecalho = resposta.headers.get("Retry-After")
espera = float(cabecalho) if cabecalho else 2 ** tentativa * 0.5 + random.random() * 0.3
time.sleep(espera)
raise RuntimeError("inalcancavel")<?php
// Cliente que registra o requestId e retenta so o que vale retentar.
declare(strict_types=1);
final class ErroApi extends RuntimeException
{
public function __construct(
public readonly int $status,
public readonly ?string $codigo,
public readonly ?string $requestId,
public readonly array $erros,
string $mensagem
) {
parent::__construct($mensagem);
}
/** 429 e 5xx passam; o resto e defeito seu e retentar nao ajuda. */
public function retentavel(): bool
{
return $this->status === 429 || $this->status >= 500;
}
}
function chamar(string $metodo, string $caminho, array $opcoes = [], int $tentativas = 4): ?array
{
$base = 'https://api.adigitalmax.com.br/v1';
for ($tentativa = 1; $tentativa <= $tentativas; $tentativa++) {
$cabecalhosRecebidos = [];
$ch = curl_init($base . $caminho);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $metodo,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => array_merge(
['Authorization: Bearer ' . getenv('ADM_CHAVE'), 'Accept: application/json'],
$opcoes['headers'] ?? []
),
CURLOPT_HEADERFUNCTION => function ($ch, $linha) use (&$cabecalhosRecebidos) {
$partes = explode(':', $linha, 2);
if (count($partes) === 2) {
$cabecalhosRecebidos[strtolower(trim($partes[0]))] = trim($partes[1]);
}
return strlen($linha);
},
]);
if (isset($opcoes['body'])) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $opcoes['body']);
}
$bruto = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status === 204) {
return null;
}
$corpo = json_decode((string) $bruto, true) ?: [];
// Registre SEMPRE, e nao so no erro.
error_log(json_encode([
'rota' => $caminho, 'status' => $status,
'requestId' => $corpo['requestId'] ?? null, 'tentativa' => $tentativa,
]));
if ($status >= 200 && $status < 300) {
return $corpo;
}
$erro = new ErroApi(
$status,
$corpo['codigo'] ?? null,
$corpo['requestId'] ?? null,
$corpo['erros'] ?? [],
sprintf('%s: %s', $corpo['codigo'] ?? '?', $corpo['title'] ?? '?')
);
if (!$erro->retentavel() || $tentativa === $tentativas) {
throw $erro;
}
// Retry-After nao e sugestao: obedeca quando vier.
$espera = isset($cabecalhosRecebidos['retry-after'])
? (float) $cabecalhosRecebidos['retry-after']
: (2 ** $tentativa) * 0.5 + (mt_rand(0, 300) / 1000);
usleep((int) ($espera * 1_000_000));
}
throw new RuntimeException('inalcancavel');
}Catálogo por código
Os dezesseis valores estáveis de codigo. Ramifique por eles: um mesmo status
HTTP carrega mais de um código, e é o código que diz o que fazer.
codigo |
HTTP | Significa | O que fazer |
|---|---|---|---|
NAO_AUTENTICADO | 401 |
Nenhuma credencial chegou, ou o cabeçalho está malformado. | Conferir o Authorization. Não retente: nada muda entre tentativas. |
CREDENCIAL_INVALIDA | 401 |
A credencial chegou e não vale: chave revogada, segredo errado ou token expirado. | Em chave de API, gerar outra. Em OAuth, renovar o token e refazer. |
MFA_OBRIGATORIO | 401 |
A organização exige segundo fator e a sessão não o cumpriu. | Só aparece na sessão do painel. Integração por chave de API não encontra este código. |
SENHA_FRACA | 422 |
A senha proposta não passa na política. | Idem: é do fluxo de painel, não da integração. |
TOKEN_INVALIDO | 401 |
Token de uso único inválido, expirado ou já consumido: link de assinatura, convite ou redefinição de senha. | Emitir outro. Link de assinatura se renova com o reenvio do convite. |
ESCOPO_INSUFICIENTE | 403 |
A chave é válida, e não tem o escopo que a operação exige. | Comparar os escopos da chave com o exigido na referência. Ampliar escopo é criar uma chave nova, no painel. |
PAPEL_INSUFICIENTE | 403 |
O escopo está certo, e o papel do dono da credencial não alcança a operação. | Este é o único 403 sobre recurso da própria organização, e ele é acionável: peça acesso ao administrador. |
NAO_ENCONTRADO | 404 |
O recurso não existe, ou é de outra organização, ou já foi excluído. | Conferir o identificador e o ambiente. Veja a seção dedicada. |
ENTRADA_INVALIDA | 422 |
A requisição está bem formada e o conteúdo não passa nas regras. | Ler erros, que aponta campo por campo. É o único código que traz o motivo específico. |
DOCUMENTO_IMUTAVEL | 409 |
Você tentou alterar um documento que saiu do rascunho. | Conteúdo já apresentado a alguém não muda, porque mudá-lo destruiria a prova. Cancele e crie outro. |
CERTIFICADO_INDISPONIVEL | 409 |
Você pediu o certificado de um documento que ainda não foi concluído. | Esperar o evento document.completed. Certificado parcial seria prova de algo que não aconteceu. |
AUTENTICACAO_INCOMPLETA | 403 |
O signatário tentou assinar sem cumprir todos os fatores exigidos. | Aparece no fluxo da página de assinatura. Se você monta a própria tela com metodoEntrega: "LINK", trate cumprindo os fatores pendentes. |
LINK_INDISPONIVEL | 410 |
O link de assinatura não vale mais: o documento foi cancelado, expirou, ou aquele signatário já assinou. | Ler o estado do documento antes de decidir. Não é erro técnico; é o fluxo tendo terminado. |
FRANQUIA_ESGOTADA | 402 |
A franquia de envelopes do plano acabou e o excedente não está habilitado. | Não retente: vai falhar igual. Avise quem cuida do contrato, ou habilite o excedente no painel. |
LIMITE_EXCEDIDO | 429 |
Passou do limite de requisições da janela. | Esperar o que diz o Retry-After. Veja Limites. |
CONFLITO | 409 |
A operação não cabe no estado atual do recurso, fora dos casos já cobertos acima. | Ler o estado antes de agir. Enviar documento já enviado e excluir pasta com conteúdo caem aqui. |
Além dos códigos acima, a borda pode responder antes de a aplicação ver a requisição:
413 quando o arquivo passa de 20 MB, e 503 em manutenção.
Esses dois podem chegar sem o corpo de problem details, porque quem os emite é
o servidor web. Trate corpo ausente como caso possível no seu cliente.
Erros de validação, em detalhe
ENTRADA_INVALIDA é o único código que devolve erros, e é onde
você vai passar a maior parte do tempo durante a integração. O formato é um arranjo de
objetos com campo e mensagem, usando notação de ponto para chegar
dentro de arranjo.
{
"type": "https://docs.adigitalmax.com.br/erros.html#entrada_invalida",
"title": "Requisição inválida",
"status": 422,
"detail": "Quatro campos precisam de correção.",
"instance": "/v1/documentos",
"codigo": "ENTRADA_INVALIDA",
"requestId": "01J9M4X7QK2ZB8N3P6R0T5V2WY",
"erros": [
{ "campo": "signatarios.0.telefone", "mensagem": "obrigatório quando o fator SMS_OTP é exigido" },
{ "campo": "campos.1.pagina", "mensagem": "página 9 não existe: o arquivo tem 7 páginas" },
{ "campo": "campos.2", "mensagem": "x + largura deve ser menor ou igual a 1" },
{ "campo": "prazoEm", "mensagem": "deve ser uma data futura" }
]
}
| Mensagem | Causa |
|---|---|
| signatário com função ASSINAR precisa de ao menos um campo do tipo ASSINATURA | Você posicionou campo de DATA ou NOME e esqueceu o de assinatura. Documento sem onde assinar não pode ser enviado. |
| obrigatório quando o fator SMS_OTP é exigido | Faltou telefone em E.164. 11988887777 não serve; precisa de +5511988887777. |
| página N não existe: o arquivo tem M páginas | Campo posicionado além do fim do PDF. Confira as páginas do documento depois de criar o rascunho. |
| x + largura deve ser menor ou igual a 1 | O campo não cabe na página. Coordenada é fração de 0 a 1, não ponto PostScript. Veja Guias. |
| o conjunto do signatário precisa conter o do documento | fatoresExigidos do signatário reforça o do documento, e nunca reduz. Você tirou um fator em vez de acrescentar. |
| nível SEGURA exige ao menos um fator de OTP | fatoresExigidos com apenas EMAIL e nivelAssinatura: "SEGURA". Acrescente SMS_OTP ou TOTP. |
| ordem duplicada em documento ordenado | Dois signatários com a mesma ordem e ordenado: true. Em documento paralelo a ordem é ignorada e não dá erro. |
| arquivo protegido por senha | O PDF tem senha de abertura ou de permissões. Remova antes de enviar. |
| o tipo real do arquivo não é PDF | O tipo é conferido pelo conteúdo, e não pela extensão nem pelo cabeçalho declarado. Renomear para .pdf não resolve. |
| deve ser uma data futura | prazoEm no passado, quase sempre por fuso: 2026-09-30 sem fuso vira meia-noite UTC, que já passou no Brasil. |
| valor desconhecido para a enumeração | Erro de digitação ou valor de outra plataforma. DELIVERY_METHOD_EMAIL aqui é EMAIL; veja Migração. |
Por que 404 e não 403 em recurso de outra organização
Quando você pede um documento que existe, mas pertence a outra organização, a API responde
404 com corpo e latência idênticos aos de um identificador que nunca existiu.
É deliberado, e está escrito no contrato como princípio.
Responder 403 nesse caso significaria "existe, e você não pode", e isso é um
oráculo de existência: permite enumerar identificadores válidos e confirmar que um contrato
específico existe na plataforma. Numa plataforma de contratos, a simples confirmação de que
um documento existe já é comercialmente sensível.
A consequência prática para você: um 404 inesperado tem três causas, e vale
checar nesta ordem. Ambiente trocado, com chave de sandbox pedindo
documento de produção, que é a causa mais comum; identificador de outra organização; ou
documento excluído.
403, quando aparece, é sempre sobre o seu próprio recurso, com o
código ESCOPO_INSUFICIENTE ou PAPEL_INSUFICIENTE. Esse é
acionável, e a distinção entre os dois códigos diz exatamente o que corrigir: o escopo da
chave ou o papel de quem a criou.
O que retentar, e o que não
| Situação | Retentar? | Como |
|---|---|---|
LIMITE_EXCEDIDO (429) | Sim | Espere o Retry-After. Ele é exato, não estimativa. |
500, 502, 503, 504 | Sim | Espera crescente com variação aleatória, até três vezes. Sem a variação, todos os seus processos retentam no mesmo instante. |
| Timeout de conexão | Sim, com cuidado | Sempre com Idempotency-Key. Você não sabe se a requisição chegou. |
NAO_AUTENTICADO, ESCOPO_INSUFICIENTE, PAPEL_INSUFICIENTE | Não | Nada muda entre uma tentativa e outra. Corrija a credencial, o escopo ou o papel. |
CREDENCIAL_INVALIDA em OAuth | Uma vez | Renove o token e refaça a chamada. Se falhar de novo, o consentimento foi revogado. |
FRANQUIA_ESGOTADA | Não | Retentar não cria franquia. Alerte quem cuida do contrato. |
NAO_ENCONTRADO, CONFLITO, DOCUMENTO_IMUTAVEL, ENTRADA_INVALIDA | Não | São defeitos da requisição ou do estado. Corrija e mande de novo, uma vez. |
CERTIFICADO_INDISPONIVEL | Não em laço | Espere o webhook document.completed, em vez de sondar até aparecer. |
Timeout não significa que a operação falhou. Significa que você não
soube o resultado. Se a criação do documento chegou ao servidor e a resposta se perdeu
na volta, retentar sem Idempotency-Key cria um segundo documento e consome
um segundo envelope. Com a chave, a segunda chamada devolve o primeiro resultado.
Como pedir ajuda
Escreva para contato@adigitalmax.com.br. Com as seis informações abaixo, resolvemos na primeira resposta; sem elas, a primeira resposta é um pedido delas.
- O
requestIdde uma requisição que apresentou o problema. É o item mais importante: com ele lemos o que aconteceu do nosso lado, incluindo o que não vai na resposta. - O prefixo da chave usada, a parte antes do ponto, como
adx_live_9f2c7d41. Nunca a chave inteira: se você mandar, ela precisará ser revogada. - O ambiente, produção ou sandbox.
- A rota e o método, como
POST /v1/documentos. - O que você esperava e o que veio, incluindo o status HTTP e o
codigo. - Quando aconteceu, com data, hora e fuso. Se é intermitente, dois ou três horários ajudam mais que uma descrição.
Assunto: ENTRADA_INVALIDA inesperado em POST /v1/documentos
requestId: 01J9M4X7QK2ZB8N3P6R0T5V2WY
chave (prefixo): adx_live_9f2c7d41
ambiente: producao
rota: POST /v1/documentos
quando: 2026-08-16 14:32 -03:00 (e tambem as 11:07 e as 09:44)
Esperado: 201 com o documento criado.
Recebido: 422, codigo ENTRADA_INVALIDA, com
erros[0].campo = "signatarios.0.telefone"
erros[0].mensagem = "obrigatorio quando o fator SMS_OTP e exigido"
O telefone esta preenchido como "+5511988887777" no nosso payload.
Acontece so com este signatario; os outros do mesmo lote passam.
Um chamado assim é resolvido lendo o log, sem precisar reproduzir. Se você não tiver o
requestId porque ainda não o registra, comece a registrar agora: o próximo
incidente vai acontecer, e ele é a diferença entre minutos e dias.