Assinatura eletrônica dentro do seu sistema
A API do AdigitalMAX envia um PDF para assinatura, acompanha cada signatário até a conclusão e devolve o arquivo assinado com trilha de auditoria, certificado de conclusão e código de validação pública. É REST sobre HTTPS, com JSON, descrita em OpenAPI 3.1. Se você já integrou com qualquer API de pagamento, isto vai parecer familiar em dez minutos.
>>> PENDENTE DE BACKEND <<<
Esta documentação foi escrita a partir do contrato canônico
docs/CONTRATO_API.openapi.yaml, e o servidor de produção ainda não está no
ar. Nenhum exemplo desta página foi executado contra uma API real. Tudo que ainda não
pôde ser verificado está marcado com o mesmo aviso, no ponto exato em que aparece.
O que a API faz
O ciclo inteiro cabe em cinco passos, e a API existe para você automatizar todos:
-
Você cria o documento com o PDF e os signatários
Um
POSTmultipart com o arquivo, a lista de signatários, a ordem em que eles assinam e onde cada campo de assinatura fica na página. O documento nasce em rascunho e não dispara nada. -
Você confere e manda enviar
Enviar é uma chamada separada e explícita, e essa separação é deliberada: disparar por efeito colateral de uma criação é como se envia contrato errado para cliente. Entre uma coisa e outra você tem uma janela para validar.
-
Nós entregamos e autenticamos cada signatário
O signatário é externo e não tem conta na plataforma. Ele recebe um link por e-mail, WhatsApp ou SMS, passa pelos fatores que você exigiu (do simples clique em link até dois fatores simultâneos) e assina no navegador.
-
Você acompanha por webhook, não por sondagem
Cada mudança de estado vira um evento assinado com HMAC no seu endpoint. Existe consulta por
GETpara conciliação, mas quem integra bem usa webhook e economiza a cota de requisições. -
Você baixa o PDF assinado e o certificado
O arquivo final sai com o manifesto: hash do original, trilha de auditoria com IP, horário e geolocalização aproximada de cada ato, e um código que qualquer pessoa confere em
adigitalmax.com.br/validar, sem conta e sem login.
Níveis de assinatura, e como escolher
O nível é escolhido por documento, no campo nivelAssinatura, e determina o que
a plataforma exige do signatário antes de aceitar a assinatura. Escolher acima do necessário
encarece e aumenta o atrito; escolher abaixo enfraquece a prova quando ela for questionada.
| Nível | Valor | O que o produto entrega | Quando usar |
|---|---|---|---|
| Eletrônica | ELETRONICA |
E-mail mais evidências básicas: IP, horário e agente registrados. | Volume alto e risco baixo: recibo, termo de ciência, autorização interna. |
| Segura | SEGURA |
E-mail mais OTP por SMS ou TOTP, com trilha completa. Exige ao menos um fator de OTP. | Contrato de serviço, proposta comercial, termo com valor econômico. |
| Avançada | AVANCADA |
Identidade verificada, integridade e evidências reforçadas. | Crédito, locação, procuração, qualquer coisa que vá parar em juízo. |
| ICP-Brasil >>> PENDENTE DE BACKEND <<< | ICP_BRASIL |
Assinatura qualificada por prestador, com PAdES e carimbo do tempo. | Onde a lei exige assinatura qualificada. |
O nível ICP-Brasil ainda não está disponível. Ele consta do contrato para que a integração já preveja o valor do enum, porque o modelo o suporta desde o início, e não porque ele funcione hoje. Não construa fluxo comercial em cima dele antes de nós anunciarmos a data.
Primeira chamada, em menos de cinco minutos
1. Pegue uma chave de sandbox
No painel, em Configurações → API → Chaves, crie uma chave com
ambiente de teste. Ela tem o formato adx_test_<prefixo>.<segredo>,
e o valor completo aparece uma única vez, na tela da criação: guardamos apenas o hash do
segredo. Se você perder, revogue e crie outra. Detalhes de escopo, rotação e revogação estão
em Autenticação.
2. Confirme que a chave funciona
GET /v1/organizacao é a chamada mais barata da API e serve de teste de fumaça:
ela devolve a organização dona da chave. Se isto responder 200, sua
autenticação está correta e todo o resto é detalhe de payload.
Não existe rota /me. Quem chega de outras APIs de
assinatura procura por ela; aqui a identidade da credencial vem de
/organizacao, porque a chave pertence a uma organização e não a uma pessoa.
#!/usr/bin/env bash
# Confere a chave e mostra a organizacao dona dela.
# A chave sai do painel: Configuracoes > API > Chaves.
set -euo pipefail
ADM_CHAVE="${ADM_CHAVE:?defina ADM_CHAVE com a sua chave adx_test_}"
ADM_BASE="https://api.adigitalmax.com.br/v1/sandbox"
curl --fail-with-body --silent --show-error \
--request GET "$ADM_BASE/organizacao" \
--header "Authorization: Bearer $ADM_CHAVE" \
--header "Accept: application/json"// Node 18 ou mais novo. O fetch e nativo, nao ha dependencia a instalar.
// Salve como organizacao.mjs e rode com: ADM_CHAVE=... node organizacao.mjs
const CHAVE = process.env.ADM_CHAVE;
const BASE = "https://api.adigitalmax.com.br/v1/sandbox";
if (!CHAVE) {
console.error("Defina ADM_CHAVE com a sua chave adx_test_.");
process.exit(1);
}
const resposta = await fetch(`${BASE}/organizacao`, {
method: "GET",
headers: {
Authorization: `Bearer ${CHAVE}`,
Accept: "application/json",
},
});
const corpo = await resposta.json();
if (!resposta.ok) {
// Ramifique por codigo, nunca por status nem por texto.
// O requestId e o que o suporte pede. Registre sempre.
console.error("Falhou:", resposta.status, corpo.codigo, corpo.detail);
console.error("requestId:", corpo.requestId);
process.exit(1);
}
console.log("Organizacao:", corpo.nome);
console.log("Documento:", corpo.documento);
console.log("Status:", corpo.status);<?php
// PHP 8.1 ou mais novo, com a extensao curl habilitada.
// Rode com: ADM_CHAVE=... php organizacao.php
declare(strict_types=1);
$chave = getenv('ADM_CHAVE');
$base = 'https://api.adigitalmax.com.br/v1/sandbox';
if ($chave === false || $chave === '') {
fwrite(STDERR, "Defina ADM_CHAVE com a sua chave adx_test_.\n");
exit(1);
}
$ch = curl_init($base . '/organizacao');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $chave,
'Accept: application/json',
],
CURLOPT_TIMEOUT => 30,
]);
$bruto = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$corpo = json_decode((string) $bruto, true);
if ($status !== 200) {
fwrite(STDERR, sprintf(
"Falhou: %d %s %s (requestId: %s)\n",
$status,
$corpo['codigo'] ?? '?',
$corpo['detail'] ?? '?',
$corpo['requestId'] ?? '?'
));
exit(1);
}
printf("Organizacao: %s\n", $corpo['nome']);
printf("Documento: %s\n", $corpo['documento']);
printf("Status: %s\n", $corpo['status']);#!/usr/bin/env python3
# Python 3.9 ou mais novo. Depende de requests: pip install requests
# Rode com: ADM_CHAVE=... python3 organizacao.py
import os
import sys
import requests
CHAVE = os.environ.get("ADM_CHAVE")
BASE = "https://api.adigitalmax.com.br/v1/sandbox"
if not CHAVE:
sys.exit("Defina ADM_CHAVE com a sua chave adx_test_.")
resposta = requests.get(
f"{BASE}/organizacao",
headers={"Authorization": f"Bearer {CHAVE}", "Accept": "application/json"},
timeout=30,
)
corpo = resposta.json()
if resposta.status_code != 200:
# Ramifique por codigo, nunca por status nem por texto.
sys.exit(
f"Falhou: {resposta.status_code} {corpo.get('codigo')} "
f"{corpo.get('detail')} (requestId: {corpo.get('requestId')})"
)
print("Organizacao:", corpo["nome"])
print("Documento:", corpo["documento"])
print("Status:", corpo["status"])Primeiro envio de verdade
São duas chamadas: criar e enviar. O corpo da criação é
multipart/form-data, porque tem arquivo dentro: a parte arquivo
leva o PDF e a parte dados leva o JSON com tudo o mais.
#!/usr/bin/env bash
# Cria o documento e dispara. Duas chamadas, de proposito.
set -euo pipefail
ADM_CHAVE="${ADM_CHAVE:?defina ADM_CHAVE}"
ADM_BASE="https://api.adigitalmax.com.br/v1/sandbox"
# A parte "dados" e um JSON; a parte "arquivo" e o PDF.
DADOS='{
"titulo": "Contrato de prestacao de servicos",
"nivelAssinatura": "SEGURA",
"fatoresExigidos": ["EMAIL", "SMS_OTP"],
"ordenado": false,
"recusavel": true,
"pararSeRecusado": true,
"lembrete": "SEMANAL",
"prazoEm": "2026-09-15T23:59:59-03:00",
"expiraEm": "2026-09-30T23:59:59-03:00",
"signatarios": [
{
"nome": "Maria Oliveira",
"email": "maria@exemplo.com.br",
"telefone": "+5511988887777",
"funcao": "ASSINAR",
"metodoEntrega": "EMAIL",
"ordem": 1
}
],
"campos": [
{ "tipo": "ASSINATURA", "pagina": 1, "x": 0.62, "y": 0.78, "largura": 0.28, "altura": 0.06 }
]
}'
DOC=$(
curl --fail-with-body --silent --show-error \
--request POST "$ADM_BASE/documentos" \
--header "Authorization: Bearer $ADM_CHAVE" \
--header "Idempotency-Key: primeiro-envio-2026-08-16-001" \
--form "dados=$DADOS;type=application/json" \
--form "arquivo=@./contrato.pdf;type=application/pdf"
)
DOC_ID=$(echo "$DOC" | jq -r '.id')
echo "Rascunho criado: $DOC_ID"
# Enviar e uma chamada separada. Ate aqui, nada saiu.
curl --fail-with-body --silent --show-error \
--request POST "$ADM_BASE/documentos/$DOC_ID/enviar" \
--header "Authorization: Bearer $ADM_CHAVE" \
--header "Idempotency-Key: primeiro-envio-2026-08-16-001-enviar" \
| jq '{id, status}'// Node 18 ou mais novo. FormData, Blob e fetch sao nativos.
// Rode com: ADM_CHAVE=... node enviar.mjs
import { readFile } from "node:fs/promises";
const CHAVE = process.env.ADM_CHAVE;
const BASE = "https://api.adigitalmax.com.br/v1/sandbox";
const dados = {
titulo: "Contrato de prestacao de servicos",
nivelAssinatura: "SEGURA",
// Dois fatores simultaneos e simplesmente informar dois valores aqui.
// Nao existe booleano de "exigir todos": a lista E o conjunto exigido.
fatoresExigidos: ["EMAIL", "SMS_OTP"],
ordenado: false,
recusavel: true,
pararSeRecusado: true,
lembrete: "SEMANAL",
prazoEm: "2026-09-15T23:59:59-03:00",
expiraEm: "2026-09-30T23:59:59-03:00",
signatarios: [
{
nome: "Maria Oliveira",
email: "maria@exemplo.com.br",
telefone: "+5511988887777",
funcao: "ASSINAR",
metodoEntrega: "EMAIL",
ordem: 1,
},
],
// Os campos ficam no nivel do documento. Em documento de um signatario
// so, o dono e inferido; com varios, aponte por signatarioId.
campos: [
{ tipo: "ASSINATURA", pagina: 1, x: 0.62, y: 0.78, largura: 0.28, altura: 0.06 },
],
};
async function api(caminho, opcoes = {}) {
const resposta = await fetch(`${BASE}${caminho}`, {
...opcoes,
headers: { Authorization: `Bearer ${CHAVE}`, Accept: "application/json", ...opcoes.headers },
});
const corpo = await resposta.json();
if (!resposta.ok) {
console.error(resposta.status, corpo.codigo, corpo.detail, corpo.requestId);
if (corpo.erros) console.error(corpo.erros);
process.exit(1);
}
return corpo;
}
const pdf = await readFile("./contrato.pdf");
const forma = new FormData();
forma.append("dados", new Blob([JSON.stringify(dados)], { type: "application/json" }));
forma.append("arquivo", new Blob([pdf], { type: "application/pdf" }), "contrato.pdf");
// 1. Criar. O documento nasce em RASCUNHO e nada e disparado.
const documento = await api("/documentos", {
method: "POST",
body: forma,
// Chave de idempotencia vinda do SEU dominio: repetir depois de um
// timeout devolve o mesmo documento, e nao um segundo.
headers: { "Idempotency-Key": "pedido-88231-criar" },
});
console.log("Rascunho:", documento.id, "|", documento.status);
// 2. Enviar. E aqui que os convites partem e um envelope e consumido.
const enviado = await api(`/documentos/${documento.id}/enviar`, {
method: "POST",
headers: { "Idempotency-Key": "pedido-88231-enviar" },
});
console.log("Enviado:", enviado.status);<?php
// PHP 8.1 ou mais novo, com curl. Rode com: ADM_CHAVE=... php enviar.php
declare(strict_types=1);
$chave = (string) getenv('ADM_CHAVE');
$base = 'https://api.adigitalmax.com.br/v1/sandbox';
$dados = [
'titulo' => 'Contrato de prestacao de servicos',
'nivelAssinatura' => 'SEGURA',
// Dois fatores simultaneos e informar dois valores aqui.
'fatoresExigidos' => ['EMAIL', 'SMS_OTP'],
'ordenado' => false,
'recusavel' => true,
'pararSeRecusado' => true,
'lembrete' => 'SEMANAL',
'prazoEm' => '2026-09-15T23:59:59-03:00',
'expiraEm' => '2026-09-30T23:59:59-03:00',
'signatarios' => [[
'nome' => 'Maria Oliveira',
'email' => 'maria@exemplo.com.br',
'telefone' => '+5511988887777',
'funcao' => 'ASSINAR',
'metodoEntrega' => 'EMAIL',
'ordem' => 1,
]],
'campos' => [[
'tipo' => 'ASSINATURA', 'pagina' => 1,
'x' => 0.62, 'y' => 0.78, 'largura' => 0.28, 'altura' => 0.06,
]],
];
function api(string $url, string $chave, array $opcoes = []): array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => array_merge(
['Authorization: Bearer ' . $chave, 'Accept: application/json'],
$opcoes['headers'] ?? []
),
CURLOPT_TIMEOUT => 60,
]);
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);
$corpo = json_decode((string) $bruto, true, 512, JSON_THROW_ON_ERROR);
if ($status < 200 || $status >= 300) {
fwrite(STDERR, sprintf(
"Falhou: %d %s %s (requestId: %s)\n",
$status,
$corpo['codigo'] ?? '?',
$corpo['detail'] ?? '?',
$corpo['requestId'] ?? '?'
));
exit(1);
}
return $corpo;
}
// 1. Criar. O documento nasce em RASCUNHO e nada e disparado.
$documento = api($base . '/documentos', $chave, [
'headers' => ['Idempotency-Key: pedido-88231-criar'],
'body' => [
'dados' => new CURLStringFile(
json_encode($dados, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
'dados.json',
'application/json'
),
'arquivo' => new CURLFile('./contrato.pdf', 'application/pdf', 'contrato.pdf'),
],
]);
printf("Rascunho: %s | %s\n", $documento['id'], $documento['status']);
// 2. Enviar. E aqui que os convites partem e um envelope e consumido.
$enviado = api($base . '/documentos/' . $documento['id'] . '/enviar', $chave, [
'headers' => ['Idempotency-Key: pedido-88231-enviar'],
]);
printf("Enviado: %s\n", $enviado['status']);#!/usr/bin/env python3
# Depende de requests: pip install requests
# Rode com: ADM_CHAVE=... python3 enviar.py
import json
import os
import sys
import requests
CHAVE = os.environ["ADM_CHAVE"]
BASE = "https://api.adigitalmax.com.br/v1/sandbox"
dados = {
"titulo": "Contrato de prestacao de servicos",
"nivelAssinatura": "SEGURA",
# Dois fatores simultaneos e informar dois valores aqui. Nao existe
# booleano de "exigir todos": a lista E o conjunto exigido.
"fatoresExigidos": ["EMAIL", "SMS_OTP"],
"ordenado": False,
"recusavel": True,
"pararSeRecusado": True,
"lembrete": "SEMANAL",
"prazoEm": "2026-09-15T23:59:59-03:00",
"expiraEm": "2026-09-30T23:59:59-03:00",
"signatarios": [
{
"nome": "Maria Oliveira",
"email": "maria@exemplo.com.br",
"telefone": "+5511988887777",
"funcao": "ASSINAR",
"metodoEntrega": "EMAIL",
"ordem": 1,
}
],
"campos": [
{"tipo": "ASSINATURA", "pagina": 1, "x": 0.62, "y": 0.78, "largura": 0.28, "altura": 0.06}
],
}
def api(metodo, caminho, **kwargs):
resposta = requests.request(
metodo,
f"{BASE}{caminho}",
headers={"Authorization": f"Bearer {CHAVE}", "Accept": "application/json",
**kwargs.pop("headers", {})},
timeout=60,
**kwargs,
)
corpo = resposta.json()
if not resposta.ok:
print(resposta.status_code, corpo.get("codigo"), corpo.get("detail"),
corpo.get("requestId"), file=sys.stderr)
if corpo.get("erros"):
print(corpo["erros"], file=sys.stderr)
sys.exit(1)
return corpo
# 1. Criar. O documento nasce em RASCUNHO e nada e disparado.
with open("./contrato.pdf", "rb") as pdf:
documento = api(
"POST",
"/documentos",
files={
"dados": ("dados.json", json.dumps(dados), "application/json"),
"arquivo": ("contrato.pdf", pdf, "application/pdf"),
},
headers={"Idempotency-Key": "pedido-88231-criar"},
)
print("Rascunho:", documento["id"], "|", documento["status"])
# 2. Enviar. E aqui que os convites partem e um envelope e consumido.
enviado = api(
"POST",
f"/documentos/{documento['id']}/enviar",
headers={"Idempotency-Key": "pedido-88231-enviar"},
)
print("Enviado:", enviado["status"])
As coordenadas de campo são fracionárias, de 0 a 1. A origem é o canto
superior esquerdo da página, e x: 0.62 significa 62% da largura, não 62
pontos. Isso faz o campo cair no mesmo lugar em A4, carta e paisagem, e é a diferença
mais comum entre a nossa API e as que usam pontos PostScript. O campo tem que caber na
página: x + largura <= 1. Há um guia dedicado em
Guias.
Coleção pronta e arquivo OpenAPI
Se o seu caminho preferido é clicar antes de programar, comece por aqui. A coleção do Postman traz as requisições desta documentação já montadas, com variáveis de ambiente para a chave e a URL base, e um exemplo de corpo em cada uma.
Coleção do Postman
Formato v2.1, importável também no Insomnia e no Bruno. Baixe, importe e preencha a variável chave.
OpenAPI 3.1 >>> PENDENTE DE BACKEND <<<
O contrato legível por máquina, servido pela própria API. Gere cliente com o gerador que você já usa.
O arquivo OpenAPI é a fonte da verdade desta documentação, e não um subproduto dela. Se alguma coisa aqui divergir do contrato, o contrato vence e o texto está errado: nos avise em contato@adigitalmax.com.br com o trecho e a operação.
Seis conceitos que aparecem em todo lugar
| Conceito | O que é |
|---|---|
| Documento | O PDF mais as regras de assinatura: quem assina, em que ordem, até quando, com que nível e com que fatores. É o recurso principal da API. |
| Envelope | Agrupa vários documentos em um único ato de envio: o signatário recebe um link só e assina o conjunto. É também a unidade da franquia do plano, e um envelope enviado conta um, tenha ele um documento ou seis. |
| Signatário | Quem assina. Não tem conta na plataforma e não precisa criar uma. É autenticado pelos fatores que você exigiu e acessa por um link de uso pessoal, com espaço de credencial separado do da sua chave. |
| Campo | Um retângulo em uma página, ligado a um signatário, que recebe a assinatura, a rubrica, o CPF, uma data ou um texto. Sem campo de assinatura o documento não pode ser enviado. |
| Trilha de auditoria | A lista imutável e encadeada por hash de tudo que aconteceu com o documento: criação, envio, abertura do link, cada fator cumprido, visualização, assinatura, recusa e download. |
| Certificado de conclusão | O pacote de evidências congelado, com hash do original e do assinado, os fatores que cada signatário cumpriu e o código de validação pública. É versionado e encadeado: emitir de novo cria uma versão nova ligada por hash à anterior, e não sobrescreve. |
Mapa da documentação
Autenticação
Chave de API por Bearer e OAuth 2.0. Escopos, rotação, revogação e o que fazer quando uma chave vaza.
Referência da API
As oitenta e três operações, com o escopo e o papel que cada uma exige, mais objetos e enumerações.
Guias de ponta a ponta
Oito receitas completas e copiáveis: enviar, posicionar, acompanhar, baixar, certificado, modelos, pastas e validação.
Webhooks
Os nove eventos, o formato do envio, como validar a assinatura HMAC, retentativa, idempotência e teste local.
Erros
O formato RFC 9457, os dezesseis códigos estáveis e o que fazer em cada um. Inclui o código de correlação.
Limites e consumo
60 requisições por minuto, paginação por cursor, 20 MB por arquivo, retenção e como medir o próprio custo.
Ambiente de testes
O que muda em relação à produção, dados de exemplo prontos e como simular recusa, expiração e falha de entrega.
Vindo do Autentique
Tabela de equivalência operação por operação, o que muda de nome, o que não existe dos dois lados e o roteiro.