Autenticação
Toda requisição à API precisa de credencial, sem exceção e sem endpoint público de
escrita. Há dois caminhos: chave de API, que é o certo para integração entre servidores,
e OAuth 2.0, que é o certo quando o seu produto age em nome de organizações que não são a
sua. Os dois chegam ao mesmo lugar, o cabeçalho Authorization: Bearer.
Os dois caminhos, e como escolher
| Chave de API | OAuth 2.0 | |
|---|---|---|
| Quando usar | O seu sistema fala com a sua própria organização. | O seu produto fala com a organização de terceiros, com o consentimento deles. |
| Quem guarda o segredo | Você, no cofre do seu servidor. | Você guarda client_id e client_secret; o token de acesso é de vida curta. |
| Validade | Até ser revogada. | Token de acesso expira em 1 hora; o refresh_token renova. |
| Revogação | No painel, imediata. | Pelo dono da organização, a qualquer momento, sem passar por você. |
| Esforço | Uma linha de cabeçalho. | Um fluxo de consentimento mais renovação de token. |
A regra prática: se você está integrando o ERP da sua empresa, use chave de API e pare de ler esta comparação. Se você está construindo um produto que outras empresas vão conectar às contas delas no AdigitalMAX, chave de API é a escolha errada, porque significa pedir que cada cliente cole um segredo permanente no seu sistema, e o dia em que você for invadido eles não terão como cortar o acesso sem falar com você.
Chave de API
Como gerar
No painel, em Configurações → API → Chaves, clique em criar, dê um nome que diga qual sistema vai usar (não "chave 1"), escolha o ambiente e marque só os escopos de que a integração precisa. Gerar chave exige papel PROPRIETARIO ou ADMINISTRADOR.
A chave tem duas partes separadas por um ponto, e o ambiente vai no próprio prefixo, de propósito:
adx_live_<prefixo>.<segredo> # producao
adx_test_<prefixo>.<segredo> # ambiente de teste
# Exemplo:
adx_live_9f2c7d41.b8e6a03f5c19d7b284e60af3a715c8d2
# A parte antes do ponto e o PREFIXO: e ela que aparece na lista de
# chaves e no log de chamadas, e e por ela que o servidor localiza a
# chave, com indice. A parte depois do ponto e o SEGREDO, e dele a
# plataforma guarda apenas o hash.
O valor completo aparece uma única vez. Guardamos apenas o hash do segredo, então nem o suporte consegue reexibir. Se você fechou a tela sem copiar, o caminho é revogar e criar outra. Isso é decisão de segurança, não limitação de interface: chave recuperável é chave que vaza junto com o banco.
A separação em duas partes também tem razão técnica: a busca no servidor é pelo prefixo, que é indexado, e a comparação do segredo é feita em tempo constante. Comparação de cadeia comum em endpoint público é um vetor mensurável.
As rotas de gerência de chave não aceitam chave de API.
/chaves-api só responde à sessão do painel. Uma chave que pudesse criar
outra chave tornaria a revogação inútil: quem roubasse uma emitiria outra antes de você
cortar a primeira.
O cabeçalho
Uma linha, em toda requisição, incluindo as de leitura. Não existe autenticação por
parâmetro de URL: chave em query string acaba em log de servidor, em histórico de
navegador e em cabeçalho Referer, e por isso a API recusa com
401 mesmo que a chave esteja correta.
POST /v1/documentos HTTP/1.1
Host: api.adigitalmax.com.br
Authorization: Bearer adx_live_9f2c7d41.b8e6a03f5c19d7b284e60af3a715c8d2
Accept: application/json
Content-Type: application/json
Idempotency-Key: pedido-88231
User-Agent: ERP-Contratos/2.4 (+https://empresa.com.br)
O User-Agent não é obrigatório, mas identifique seu sistema nele. Quando
algo der errado às três da manhã, ele é o que permite ao nosso suporte distinguir a sua
integração de outras vinte no mesmo log.
Escopos
Escopo é o que a chave pode fazer, e ele é verificado a cada requisição, antes de
qualquer regra de papel. Uma chave só de leitura que tente criar documento recebe
403 com ESCOPO_INSUFICIENTE, e nenhum dado é tocado.
| Escopo | Concede |
|---|---|
organizacao:ler | Ler dados da organização, plano e marca. É o escopo de GET /v1/organizacao, a chamada de teste de fumaça da integração. |
organizacao:escrever | Alterar a organização e gerenciar membros. |
documentos:ler | Listar e consultar documentos, campos e certificado. |
documentos:escrever | Criar, editar, enviar, reenviar, cancelar, mover e excluir documento, e definir campos. |
signatarios:ler | Consultar signatários e o estado de autenticação de cada um. |
signatarios:escrever | Adicionar, corrigir e remover signatário. |
pastas:ler | Listar pastas e o conteúdo delas. |
pastas:escrever | Criar, renomear, mover e excluir pasta. |
modelos:ler | Listar e consultar modelos de documento. |
modelos:escrever | Criar, editar e excluir modelo. |
arquivos:ler | Baixar o arquivo do documento: original, assinado, PAdES e certificado. |
auditoria:ler | Ler a trilha de auditoria do documento. É separado de documentos:ler: uma chave pode ler documentos sem ler a trilha deles. |
webhooks:gerenciar | Cadastrar, editar e remover webhook, e consultar o histórico de entregas. |
consumo:ler | Ler a medição de consumo do período e o extrato, mais plano e faturas. |
Duas combinações cobrem a maioria das integrações reais. Para enviar e
acompanhar: documentos:ler, documentos:escrever,
signatarios:escrever e arquivos:ler. Para um
painel de conciliação somente leitura: documentos:ler,
signatarios:ler, auditoria:ler e consumo:ler.
Escopo é uma coisa, papel é outra
A chave herda o papel do usuário que a criou, e as duas verificações acontecem em
sequência: primeiro o escopo da chave, depois o papel do dono dela. Uma chave com
consumo:ler criada por um ADMINISTRADOR
continua sem acesso a faturamento, porque faturamento é do
PROPRIETARIO. Marcar o escopo não promove
ninguém.
| Papel | Alcance |
|---|---|
PROPRIETARIO | Tudo, incluindo faturamento, transferência de propriedade e exclusão da organização. |
ADMINISTRADOR | Usuários, papéis, integrações, chaves, webhooks e configuração. Não alcança faturamento por padrão. |
OPERADOR | Cria, envia, cancela e move os próprios documentos. Não gerencia a organização nem cria chave. |
VISUALIZADOR | Somente leitura e download do que lhe foi dado acesso. Não envia nem cancela. |
A referência marca, em cada operação, o escopo e o papel mínimo exigidos. Quando os dois aparecem, os dois precisam ser satisfeitos.
OAuth 2.0
>>> PENDENTE DE BACKEND <<<
O fluxo de OAuth está especificado e ainda não foi exercido contra o servidor. Os nomes de parâmetro seguem a RFC 6749 e não devem mudar; as URLs e o formato exato do token podem ajustar até a publicação. Se você vai construir sobre OAuth agora, fale conosco antes para receber aviso de qualquer mudança.
Registrar o cliente
Aplicações que integram organizações de terceiros são registradas por nós, e não
pelo painel. Você nos envia o nome da aplicação, a URL de redirecionamento e os
escopos que vai pedir, e recebe client_id e client_secret.
A URL de redirecionamento é comparada por igualdade exata, incluindo esquema, porta e
barra final.
Fluxo de código de autorização
É o fluxo para agir em nome de um usuário. Use PKCE mesmo em cliente confidencial: ele custa duas linhas e fecha a interceptação do código na volta.
GET https://app.adigitalmax.com.br/oauth/autorizar
?response_type=code
&client_id=cli_01J9M4X7QK2ZB8N3P6R0T5V2WY
&redirect_uri=https%3A%2F%2Fseu-produto.com.br%2Fcallback%2Fadigitalmax
&scope=documentos:ler+documentos:escrever+arquivos:ler
&state=8f14e45fceea167a5a36dedd4bea2543
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
O usuário vê quais escopos você está pedindo e em qual organização, aprova, e volta para
o seu redirect_uri com ?code=...&state=.... Confira o
state antes de qualquer outra coisa: ele é a sua defesa contra falsificação
de requisição na volta.
#!/usr/bin/env bash
# 2. Trocar o codigo por um token de acesso.
set -euo pipefail
curl --fail-with-body --silent --show-error \
--request POST "https://api.adigitalmax.com.br/v1/oauth/token" \
--header "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$CODIGO_RECEBIDO" \
--data-urlencode "redirect_uri=https://seu-produto.com.br/callback/adigitalmax" \
--data-urlencode "client_id=$ADM_CLIENT_ID" \
--data-urlencode "client_secret=$ADM_CLIENT_SECRET" \
--data-urlencode "code_verifier=$VERIFICADOR_PKCE"// Node 18 ou mais novo. Troca o codigo por token e guarda a renovacao.
const BASE = "https://api.adigitalmax.com.br/v1";
export async function trocarCodigoPorToken(codigo, verificadorPkce) {
const corpo = new URLSearchParams({
grant_type: "authorization_code",
code: codigo,
redirect_uri: "https://seu-produto.com.br/callback/adigitalmax",
client_id: process.env.ADM_CLIENT_ID,
client_secret: process.env.ADM_CLIENT_SECRET,
code_verifier: verificadorPkce,
});
const resposta = await fetch(`${BASE}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: corpo,
});
const dados = await resposta.json();
if (!resposta.ok) {
throw new Error(`OAuth falhou: ${dados.error} ${dados.error_description}`);
}
// Guarde expira_em como horario absoluto, e nao como duracao: se o
// processo dormir, a duracao relativa passa a mentir.
return {
acesso: dados.access_token,
renovacao: dados.refresh_token,
expiraEm: Date.now() + dados.expires_in * 1000,
escopos: dados.scope.split(" "),
organizacaoId: dados.organizacaoId,
};
}
export async function renovar(tokenDeRenovacao) {
const resposta = await fetch(`${BASE}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: tokenDeRenovacao,
client_id: process.env.ADM_CLIENT_ID,
client_secret: process.env.ADM_CLIENT_SECRET,
}),
});
if (!resposta.ok) throw new Error("Renovacao recusada; refaca o consentimento.");
return resposta.json();
}<?php
// Troca do codigo por token, sem dependencia externa.
declare(strict_types=1);
function trocarCodigoPorToken(string $codigo, string $verificadorPkce): array
{
$campos = http_build_query([
'grant_type' => 'authorization_code',
'code' => $codigo,
'redirect_uri' => 'https://seu-produto.com.br/callback/adigitalmax',
'client_id' => getenv('ADM_CLIENT_ID'),
'client_secret' => getenv('ADM_CLIENT_SECRET'),
'code_verifier' => $verificadorPkce,
]);
$ch = curl_init('https://api.adigitalmax.com.br/v1/oauth/token');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $campos,
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
CURLOPT_TIMEOUT => 30,
]);
$bruto = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$dados = json_decode((string) $bruto, true, 512, JSON_THROW_ON_ERROR);
if ($status !== 200) {
throw new RuntimeException(sprintf(
'OAuth falhou: %s %s',
$dados['error'] ?? '?',
$dados['error_description'] ?? ''
));
}
return [
'acesso' => $dados['access_token'],
'renovacao' => $dados['refresh_token'],
// Horario absoluto, nunca duracao relativa guardada em banco.
'expira_em' => time() + (int) $dados['expires_in'],
'escopos' => explode(' ', $dados['scope']),
];
}#!/usr/bin/env python3
"""Troca do codigo de autorizacao por token de acesso."""
import os
import time
import requests
BASE = "https://api.adigitalmax.com.br/v1"
def trocar_codigo_por_token(codigo: str, verificador_pkce: str) -> dict:
resposta = requests.post(
f"{BASE}/oauth/token",
data={
"grant_type": "authorization_code",
"code": codigo,
"redirect_uri": "https://seu-produto.com.br/callback/adigitalmax",
"client_id": os.environ["ADM_CLIENT_ID"],
"client_secret": os.environ["ADM_CLIENT_SECRET"],
"code_verifier": verificador_pkce,
},
timeout=30,
)
dados = resposta.json()
if resposta.status_code != 200:
raise RuntimeError(
f"OAuth falhou: {dados.get('error')} {dados.get('error_description')}"
)
return {
"acesso": dados["access_token"],
"renovacao": dados["refresh_token"],
# Horario absoluto. Duracao relativa mente assim que o processo dorme.
"expira_em": time.time() + dados["expires_in"],
"escopos": dados["scope"].split(" "),
}
def renovar(token_de_renovacao: str) -> dict:
resposta = requests.post(
f"{BASE}/oauth/token",
data={
"grant_type": "refresh_token",
"refresh_token": token_de_renovacao,
"client_id": os.environ["ADM_CLIENT_ID"],
"client_secret": os.environ["ADM_CLIENT_SECRET"],
},
timeout=30,
)
resposta.raise_for_status()
return resposta.json(){
"access_token": "admt_01J9M4X7QK2ZB8N3P6R0T5V2WY.eyJvcmciOiJvcmdfMDFKOU00WDdRSzJa",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "admr_01J9M4X7QK2ZB8N3P6R0T5V2X0",
"scope": "documentos:ler documentos:escrever arquivos:ler",
"organizacaoId": "8f47a0c1-3d92-4e65-b108-5a2c7e91d0f3"
}
Fluxo de credenciais de cliente
Para máquina falando com máquina dentro da sua própria organização, sem usuário no meio. Na prática ele resolve o mesmo problema que a chave de API, com a vantagem de o token expirar sozinho:
curl --request POST "https://api.adigitalmax.com.br/v1/oauth/token" \
--header "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=$ADM_CLIENT_ID" \
--data-urlencode "client_secret=$ADM_CLIENT_SECRET" \
--data-urlencode "scope=documentos:ler documentos:escrever"
Renove o token quando faltar menos de cinco minutos para expirar, e não quando receber
401. Renovar por reação transforma cada expiração em uma requisição
perdida e, sob concorrência, em várias.
Rotação e revogação
Rotacionar sem janela de indisponibilidade depende de a API aceitar mais de uma chave ativa ao mesmo tempo, e ela aceita. O procedimento tem quatro passos e nenhum deles derruba a integração:
-
Crie a chave nova
Mesmos escopos da antiga, nome que indique a data (por exemplo
erp-contratos-2026-08). As duas ficam válidas. -
Publique a chave nova no seu cofre
Variável de ambiente, gerenciador de segredos, o que você já usa. Reinicie o serviço e confirme com
GET /v1/organizacaoque o prefixo que aparece no log de chamadas do painel é o novo. -
Observe por 24 horas
O painel mostra o último uso de cada chave. Se a antiga continuar sendo usada, existe uma cópia dela em algum lugar que você esqueceu, e é exatamente isso que este passo serve para descobrir.
-
Revogue a antiga
A revogação vale imediatamente, sem carência. Requisições com a chave revogada passam a responder
401comCREDENCIAL_INVALIDA.
Rotacione por calendário, a cada seis meses, e não só quando houver suspeita. Chave que nunca rodou é chave cuja rotação ninguém sabe fazer sob pressão, e o dia do vazamento é o pior dia para descobrir isso.
Se uma chave vazar
Revogue primeiro, investigue depois. A ordem inversa é a que transforma um susto em incidente. Revogar custa uma janela curta de indisponibilidade da sua integração; não revogar custa documentos enviados em nome da sua empresa.
Na ordem, e sem pular etapa:
- Revogue a chave no painel. Vale na hora, para todas as requisições em voo.
- Crie uma chave nova com os mesmos escopos e publique no seu cofre. A integração volta.
- Leia o log de chamadas em Configurações → API → Chamadas, filtrando pelo prefixo da chave vazada. Cada linha traz horário, operação, IP e resultado. É aqui que você descobre se ela chegou a ser usada por outra pessoa.
-
Leia a trilha de auditoria da organização, filtrando por ator do tipo
CHAVE_API. Se houver documento criado ou enviado que você não reconhece, cancele imediatamente: documento cancelado não pode mais ser assinado. - Nos avise em contato@adigitalmax.com.br com o prefixo da chave e a janela de horário. Conseguimos ver o que você não vê, incluindo tentativas recusadas, e conseguimos bloquear origens.
- Procure a causa. Quase sempre é uma destas quatro: chave commitada em repositório, chave em log de aplicação, chave em variável de build exposta ao navegador, ou chave colada em ferramenta de terceiro. Enquanto a causa não for encontrada, a chave nova está na mesma trajetória da antiga.
Se a chave vazou em repositório público, considere que ela foi usada. Rastreadores automáticos varrem commits novos em segundos, e trocar o commit não resolve: o histórico já foi lido.
Por que a chave nunca vai para o frontend
Esta é a regra que mais gente quebra, e ela não tem exceção sofisticada. Qualquer coisa que chegue ao navegador do usuário é pública: minificar não esconde, variável de ambiente do bundler não esconde, ofuscar não esconde. Basta abrir a aba de rede ou procurar a string no arquivo JavaScript.
O que a chave permite não é pouco. Com documentos:escrever, quem a tiver
envia documento em nome da sua empresa, para qualquer destinatário, com o seu logotipo
e o seu remetente. Com arquivos:ler, baixa todo contrato assinado da sua
organização. Não existe escopo de "só o meu documento": o escopo é da organização.
| Você quer | Faça |
|---|---|
| Enviar documento a partir de uma tela do seu app | A tela chama o seu backend; o seu backend chama a nossa API com a chave. O navegador nunca vê a credencial. |
| Mostrar ao seu usuário o estado de um documento | O seu backend consulta e devolve só o que aquele usuário pode ver. Recebendo webhook, você nem precisa consultar. |
| Deixar o usuário baixar o PDF assinado | Peça a URL de download no backend e redirecione o navegador para ela. A URL é de vida curta e de uso único, e é isso que a torna segura de expor. |
| Deixar qualquer pessoa conferir a autenticidade | Mande para adigitalmax.com.br/validar, que é público por desenho e não usa credencial nenhuma. Veja Guias. |
Aplicativo móvel é o mesmo caso, com um agravante: o binário pode ser descompilado com calma, offline, e a chave embutida nele vale para todas as instalações ao mesmo tempo, inclusive as que você não consegue atualizar.