Pular para o conteúdo
AdigitalMAX AdigitalMAX API v1
Construir

Guias de ponta a ponta

Oito receitas completas. Cada bloco de código roda como está, sem recorte e sem pseudocódigo: você preenche a chave e o caminho do PDF, e ele funciona. Todos os exemplos apontam para o sandbox; trocar para produção é remover o /sandbox da URL base e usar uma chave adx_live_.

>>> PENDENTE DE BACKEND <<<

Estes exemplos seguem o contrato canônico docs/CONTRATO_API.openapi.yaml e não foram executados contra um servidor real. Serão conferidos chamada por chamada quando o serviço subir.

1. Enviar um documento para assinatura

São sempre dois passos: criar como rascunho e depois enviar. Não existe atalho de "criar e disparar", e a ausência dele é deliberada: disparar por efeito colateral de uma criação é como se envia contrato errado para cliente. Entre os dois passos você tem uma janela para validar.

// enviar.mjs: Node 18 ou mais novo, sem dependencia.
//   ADM_CHAVE=... node enviar.mjs ./contrato.pdf

import { readFile } from "node:fs/promises";

const CHAVE = process.env.ADM_CHAVE;
const BASE = "https://api.adigitalmax.com.br/v1/sandbox";

/** Cliente minimo com tratamento de erro de verdade. */
async function api(caminho, opcoes = {}) {
  const resposta = await fetch(`${BASE}${caminho}`, {
    ...opcoes,
    headers: { Authorization: `Bearer ${CHAVE}`, Accept: "application/json", ...opcoes.headers },
  });

  if (resposta.status === 204) return null;
  const corpo = await resposta.json();

  if (!resposta.ok) {
    const erro = new Error(`${corpo.codigo}: ${corpo.title}`);
    erro.status = resposta.status;
    erro.codigo = corpo.codigo;       // ramifique por ele, nao pelo status
    erro.requestId = corpo.requestId; // guarde: e o que o suporte pede
    erro.erros = corpo.erros ?? [];
    throw erro;
  }
  return corpo;
}

const DADOS = {
  titulo: "Contrato de prestacao de servicos",
  descricao: "Vigencia de 12 meses, renovacao automatica.",
  nivelAssinatura: "SEGURA",
  // Fatores exigidos de TODO signatario. Dois valores = dois fatores
  // simultaneos; nao existe booleano de "exigir todos".
  fatoresExigidos: ["EMAIL", "SMS_OTP"],
  ordenado: true, // Carlos so recebe depois de Maria assinar
  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", // exigido pelo fator SMS_OTP
      funcao: "ASSINAR",
      metodoEntrega: "EMAIL",
      ordem: 1,
    },
    {
      nome: "Carlos Souza",
      email: "carlos@empresa.com.br",
      telefone: "+5511977776666",
      funcao: "ASSINAR",
      metodoEntrega: "EMAIL",
      ordem: 2,
    },
  ],
  // Campos ficam no nivel do documento e apontam o dono por
  // signatarioId, que so existe depois de criar. Por isso este exemplo
  // posiciona os campos no segundo passo.
  campos: [],
};

function conferir(documento, campos) {
  const problemas = [];

  for (const s of documento.signatarios) {
    if (s.funcao === "ASSINAR") {
      const tem = campos.some((c) => c.signatarioId === s.id && c.tipo === "ASSINATURA");
      if (!tem) problemas.push(`${s.nome} nao tem campo de assinatura`);
    }
    const fatores = s.fatoresExigidos ?? documento.fatoresExigidos ?? [];
    if (fatores.includes("SMS_OTP") && !s.telefone) {
      problemas.push(`${s.nome} exige SMS_OTP e nao tem telefone`);
    }
  }

  for (const c of campos) {
    // O campo tem que caber na pagina, e o servidor recusa se nao couber.
    if (c.x + c.largura > 1 || c.y + c.altura > 1) {
      problemas.push(`campo na pagina ${c.pagina} nao cabe na pagina`);
    }
  }
  return problemas;
}

async function main() {
  const caminhoPdf = process.argv[2] || "./contrato.pdf";
  const pdf = await readFile(caminhoPdf);

  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. Nasce em RASCUNHO; nada e disparado.
  const rascunho = await api("/documentos", {
    method: "POST",
    body: forma,
    headers: { "Idempotency-Key": "pedido-88231-criar" },
  });
  console.log("Rascunho:", rascunho.id, "|", rascunho.status);

  // 2. Posicionar os campos, agora que os signatarios tem id.
  const [maria, carlos] = rascunho.signatarios;
  const campos = [
    { signatarioId: maria.id,  tipo: "ASSINATURA", pagina: 7, x: 0.10, y: 0.72, largura: 0.34, altura: 0.07 },
    { signatarioId: maria.id,  tipo: "DATA",       pagina: 7, x: 0.10, y: 0.82, largura: 0.20, altura: 0.03 },
    { signatarioId: carlos.id, tipo: "ASSINATURA", pagina: 7, x: 0.56, y: 0.72, largura: 0.34, altura: 0.07 },
  ];

  await api(`/documentos/${rascunho.id}/campos`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ campos }),
  });

  // 3. Conferir antes de qualquer coisa sair.
  const problemas = conferir(rascunho, campos);
  if (problemas.length > 0) {
    console.error("Nao vou enviar. Problemas encontrados:");
    problemas.forEach((p) => console.error(" -", p));
    // O rascunho fica la, editavel, e nao consumiu envelope.
    process.exit(1);
  }

  // 4. Enviar. E aqui que um envelope e consumido.
  const enviado = await api(`/documentos/${rascunho.id}/enviar`, {
    method: "POST",
    headers: { "Idempotency-Key": "pedido-88231-enviar" },
  });

  console.log("Enviado. Estado:", enviado.status);
  for (const s of enviado.signatarios) {
    console.log(` - ${s.nome}: ${s.status}${s.link ? " " + s.link : ""}`);
  }
}

main().catch((e) => {
  console.error(`Falhou (${e.status}) ${e.codigo}: ${e.message}`);
  if (e.erros?.length) console.error("Campos:", e.erros);
  console.error("requestId:", e.requestId);
  process.exit(1);
});

2. Posicionar os campos sem abrir o PDF

A coordenada é fracionária, de 0 a 1, com origem no canto superior esquerdo da página, que é a convenção do DOM e a que o editor usa ao arrastar o campo sobre a prévia. x: 0 é a margem esquerda, x: 1 é a direita; y: 0 é o topo, y: 1 é o rodapé. Largura e altura são frações das dimensões da página.

Relativa e não absoluta em pontos porque o mesmo posicionamento precisa valer para A4, carta e paisagem, e porque o PDF pode ser reescalado na renderização. Ponto fixo quebra em silêncio quando o cliente sobe o mesmo contrato em outro tamanho de papel. O campo tem que caber: x + largura <= 1 e y + altura <= 1, e violar isso devolve ENTRADA_INVALIDA.

Converter de pontos para fração
// A4 em pontos: 595.28 x 841.89. A origem do PDF fica embaixo a
// esquerda; a nossa fica em cima a esquerda, entao o y se inverte.

function paraFracao({ xPt, yPt, larguraPt, alturaPt }, larguraPagina = 595.28, alturaPagina = 841.89) {
  return {
    x: xPt / larguraPagina,
    // A inversao do eixo Y e o erro mais comum desta conversao.
    y: (alturaPagina - yPt - alturaPt) / alturaPagina,
    largura: larguraPt / larguraPagina,
    altura: alturaPt / alturaPagina,
  };
}

// Um retangulo de assinatura de 200x40 pt, a 60 pt da esquerda e
// 120 pt do rodape:
console.log(paraFracao({ xPt: 60, yPt: 120, larguraPt: 200, alturaPt: 40 }));
// { x: 0.1008, y: 0.8100, largura: 0.3360, altura: 0.0475 }
Posições que funcionam bem na prática, para você não começar do zero.
Ondexylarguraaltura
Assinatura, coluna esquerda0.100.720.340.07
Assinatura, coluna direita0.560.720.340.07
Assinatura, centralizada0.330.750.340.07
Data, abaixo da assinatura0.100.820.200.03
Rubrica, rodapé direito0.820.930.120.04

Para rubricar todas as páginas, repita o campo RUBRICA variando só pagina. Não existe atalho de "todas as páginas" no contrato, e isso é intencional: rubrica é ato por página, e a trilha registra cada uma. O campo z resolve sobreposição, quando dois campos disputam a mesma área.

3. Acompanhar até a conclusão

Use webhook. Sondar a API de minuto em minuto para descobrir que nada mudou queima a sua cota de 60 requisições por minuto e não deixa o documento ser assinado mais rápido. O código do receptor, com validação de assinatura, está em Webhooks.

Sondagem tem um lugar legítimo: conciliação. Uma vez por hora, ou uma vez por dia, você pergunta à API o estado do que ainda está aberto no seu banco e corrige divergências causadas por webhook perdido. É isto que o exemplo abaixo faz, e ele é o complemento do webhook, não o substituto.

#!/usr/bin/env python3
"""Conciliacao horaria: percorre o que ainda esta aberto e reporta.

Roda em cron, uma vez por hora. Nao substitui webhook: existe para
achar o que o webhook perdeu.

    pip install requests
    ADM_CHAVE=... python3 conciliar.py
"""

import os
import sys
import time

import requests

CHAVE = os.environ["ADM_CHAVE"]
BASE = "https://api.adigitalmax.com.br/v1/sandbox"

sessao = requests.Session()
sessao.headers.update({"Authorization": f"Bearer {CHAVE}", "Accept": "application/json"})


def paginar(caminho: str, **parametros):
    """Percorre todas as paginas de uma listagem por cursor."""
    cursor = None
    while True:
        if cursor:
            parametros["cursor"] = cursor
        resposta = sessao.get(f"{BASE}{caminho}", params=parametros, timeout=30)

        # 429: respeite o Retry-After em vez de tentar de novo na hora.
        if resposta.status_code == 429:
            espera = int(resposta.headers.get("Retry-After", "5"))
            print(f"Limite atingido; aguardando {espera}s", file=sys.stderr)
            time.sleep(espera)
            continue

        resposta.raise_for_status()
        corpo = resposta.json()

        yield from corpo["itens"]

        # Pare quando proximoCursor vier nulo ou ausente, e nunca porque
        # "a pagina veio curta": sao coisas diferentes.
        cursor = corpo.get("proximoCursor")
        if not cursor:
            return


def main() -> None:
    abertos = 0

    for status in ("AGUARDANDO_ASSINATURAS", "PARCIALMENTE_ASSINADO"):
        for resumo in paginar("/documentos", status=status, limite=100):
            abertos += 1
            detalhe = sessao.get(f"{BASE}/documentos/{resumo['id']}", timeout=30).json()

            assinaram = sum(1 for s in detalhe["signatarios"] if s["status"] == "ASSINADO")
            total = len(detalhe["signatarios"])

            # Quem falhou na entrega e o unico caso realmente acionavel:
            # quase sempre e endereco errado, e tem conserto.
            falhas = [s for s in detalhe["signatarios"] if s["status"] == "FALHA_ENTREGA"]

            print(f"{detalhe['id']} {assinaram}/{total} {detalhe['status']}")

            for s in falhas:
                print(f"  !! entrega falhou para {s['nome']} <{s.get('email')}>")
                # PATCH em /documentos/{id}/signatarios/{id} corrige o
                # e-mail, e POST em /reenviar dispara de novo.

            if detalhe.get("prazoEm"):
                print(f"  prazo: {detalhe['prazoEm']}")

    print(f"\n{abertos} documentos abertos.")


if __name__ == "__main__":
    main()

4. Baixar o PDF assinado

O arquivo assinado passa a existir quando o documento chega a ASSINADO, ou seja, quando o evento document.completed chega. É uma rota só, com o tipo em parâmetro de consulta: GET /documentos/{documentoId}/arquivo?tipo=ASSINADO.

Do outro lado, a API resolve o objeto dentro do escopo da organização, verifica o direito e só então delega a entrega ao servidor web por X-Accel-Redirect. Não existe caminho público previsível para o arquivo, e o diretório nunca é servido como estático.

#!/usr/bin/env bash
# Baixa o PDF assinado e confere o hash contra o certificado.
#   ADM_CHAVE=... ./baixar.sh 6f2a1c94-8b3d-4e57-9a20-71c5f0d3ab48

set -euo pipefail

ADM_CHAVE="${ADM_CHAVE:?defina ADM_CHAVE}"
ADM_BASE="https://api.adigitalmax.com.br/v1/sandbox"
DOC_ID="${1:?informe o id do documento}"
AUTH=(--header "Authorization: Bearer $ADM_CHAVE")

# --location segue eventual redirecionamento interno e grava o PDF.
curl --location --fail-with-body --silent --show-error "${AUTH[@]}" \
  --output "$DOC_ID-assinado.pdf" \
  --write-out "HTTP %{http_code}, %{size_download} bytes\n" \
  "$ADM_BASE/documentos/$DOC_ID/arquivo?tipo=ASSINADO"

# O certificado declara o hash do documento assinado. Se divergir, o
# arquivo corrompeu no caminho e nao deve ser arquivado como prova.
ESPERADO=$(
  curl --fail-with-body --silent "${AUTH[@]}" \
    --header "Accept: application/json" \
    "$ADM_BASE/documentos/$DOC_ID/certificado" | jq -r '.hashDocumento'
)
OBTIDO=$(sha256sum "$DOC_ID-assinado.pdf" | cut -d' ' -f1)

echo "esperado: $ESPERADO"
echo "obtido:   $OBTIDO"
[ "$ESPERADO" = "$OBTIDO" ] && echo "OK" || { echo "HASH DIVERGENTE"; exit 1; }

Guarde uma cópia do PDF assinado e do certificado no seu próprio armazenamento. Nós guardamos, mas a política de retenção da sua organização pode expurgar, o seu contrato conosco pode terminar, e a prova de um contrato de doze anos precisa sobreviver ao fornecedor que a gerou. Guarde também o código de validação: com ele, a autenticidade continua conferível pela validação pública.

5. Certificado de conclusão e evidências

O certificado é o que se apresenta quando a assinatura é questionada. Ele reúne, em um pacote congelado, o hash do arquivo, a lista de signatários com os fatores que cada um cumpriu, IP, horário e dados técnicos de cada ato, e a sequência integral de eventos com o hash de cada elo.

Ele é versionado e encadeado: emitir de novo cria uma versão nova ligada por hashAnterior à anterior, e não sobrescreve. Isso é o que permite provar que a prova não foi trocada.

#!/usr/bin/env bash
# Guarda o dossie completo de um documento concluido: PDF assinado,
# certificado em PDF, certificado em JSON e a trilha inteira.
#   ADM_CHAVE=... ./dossie.sh 6f2a1c94-8b3d-4e57-9a20-71c5f0d3ab48

set -euo pipefail

ADM_CHAVE="${ADM_CHAVE:?defina ADM_CHAVE}"
ADM_BASE="https://api.adigitalmax.com.br/v1/sandbox"
DOC_ID="${1:?informe o id do documento}"
DESTINO="dossie-$DOC_ID"

mkdir -p "$DESTINO"
AUTH=(--header "Authorization: Bearer $ADM_CHAVE")

# 1. O PDF assinado.
curl --location --fail-with-body --silent "${AUTH[@]}" \
  --output "$DESTINO/assinado.pdf" \
  "$ADM_BASE/documentos/$DOC_ID/arquivo?tipo=ASSINADO"

# 2. O certificado em PDF, para anexar a um processo.
curl --location --fail-with-body --silent "${AUTH[@]}" \
  --output "$DESTINO/certificado.pdf" \
  "$ADM_BASE/documentos/$DOC_ID/arquivo?tipo=CERTIFICADO"

# 3. O mesmo certificado em JSON, para o seu sistema indexar.
curl --fail-with-body --silent "${AUTH[@]}" \
  --header "Accept: application/json" \
  "$ADM_BASE/documentos/$DOC_ID/certificado" > "$DESTINO/certificado.json"

# 4. A trilha completa, mais detalhada que o resumo do certificado.
curl --fail-with-body --silent "${AUTH[@]}" \
  --header "Accept: application/json" \
  "$ADM_BASE/documentos/$DOC_ID/auditoria?limite=100" > "$DESTINO/auditoria.json"

echo "Dossie em $DESTINO/"
jq -r '"codigo: \(.codigo)\nversao: \(.versao)\nhash do documento: \(.hashDocumento)"' \
  "$DESTINO/certificado.json"

Vale entender o que cada peça sustenta. O hash prova que o arquivo não mudou desde a assinatura. Os fatores cumpridos sustentam a autoria: é a diferença entre "alguém com acesso a este e-mail clicou" e "alguém com acesso a este e-mail e a este telefone clicou". A trilha encadeada sustenta a não-repudiação, mostrando que o signatário viu o documento antes de assinar e quanto tempo passou entre uma coisa e outra.

6. Criar e usar modelos

Se você envia o mesmo contrato com nomes diferentes, modelo é a diferença entre subir um PDF de 2 MB e posicionar campos a cada envio, e mandar um JSON de vinte linhas. Em volume, é a diferença entre uma integração que funciona e uma que estoura o limite de taxa.

Passo 1: criar o modelo, uma vez
#!/usr/bin/env bash
# O PDF tem marcadores {{cliente_nome}} e {{valor_mensal}} no texto,
# que serao substituidos na hora de instanciar.

set -euo pipefail

ADM_CHAVE="${ADM_CHAVE:?}"
ADM_BASE="https://api.adigitalmax.com.br/v1/sandbox"

MODELO='{
  "nome": "Contrato de prestacao de servicos - padrao 2026",
  "descricao": "Modelo aprovado pelo juridico em 2026-07-30. Nao editar sem revisao.",
  "escopo": "ORGANIZACAO",
  "nivelAssinatura": "SEGURA",
  "fatoresExigidos": ["EMAIL", "SMS_OTP"],
  "papeis": [
    {
      "rotulo": "Contratante",
      "funcao": "ASSINAR",
      "ordem": 1,
      "campos": [
        { "tipo": "ASSINATURA", "pagina": 7, "x": 0.10, "y": 0.72, "largura": 0.34, "altura": 0.07 },
        { "tipo": "DATA",       "pagina": 7, "x": 0.10, "y": 0.82, "largura": 0.20, "altura": 0.03 }
      ]
    },
    {
      "rotulo": "Contratada",
      "funcao": "ASSINAR",
      "ordem": 2,
      "campos": [
        { "tipo": "ASSINATURA", "pagina": 7, "x": 0.56, "y": 0.72, "largura": 0.34, "altura": 0.07 }
      ]
    }
  ]
}'

curl --fail-with-body --silent --show-error \
  --request POST "$ADM_BASE/modelos" \
  --header "Authorization: Bearer $ADM_CHAVE" \
  --form "dados=$MODELO;type=application/json" \
  --form "arquivo=@./contrato-padrao.pdf;type=application/pdf" \
  | jq '{id, nome, variaveis, papeis: [.papeis[] | {id, rotulo}]}'

A resposta traz variaveis, que é a lista de marcadores encontrados no PDF, e o id de cada papel. Guarde os dois: são o que o próximo passo consome.

// Passo 2: instanciar. Duas requisicoes de JSON puro, sem upload.
// Esta e a funcao que voce chama quando um pedido e aprovado no seu ERP.

const CHAVE = process.env.ADM_CHAVE;
const BASE = "https://api.adigitalmax.com.br/v1/sandbox";

const MODELO_ID = "3c9e7b12-4a58-4d06-9f31-2b8c5e04a7d9";
const PAPEL_CONTRATANTE = "a41f8d20-6c93-4b7e-8215-0d6a9f37c5b1";
const PAPEL_CONTRATADA = "b52a9e31-7d04-4c8f-9326-1e7b0a48d6c2";

export async function gerarContrato(pedido) {
  const cabecalhos = {
    Authorization: `Bearer ${CHAVE}`,
    "Content-Type": "application/json",
    Accept: "application/json",
  };

  // 1. Instanciar. Como toda criacao, nasce em RASCUNHO.
  const criar = await fetch(`${BASE}/modelos/${MODELO_ID}/documentos`, {
    method: "POST",
    headers: {
      ...cabecalhos,
      // O numero do pedido como chave: retentativa depois de um timeout
      // nao gera um segundo contrato.
      "Idempotency-Key": `contrato-pedido-${pedido.numero}`,
    },
    body: JSON.stringify({
      titulo: `Contrato ${pedido.cliente.nome} - ${pedido.competencia}`,
      pastaId: pedido.pastaId,
      variaveis: {
        cliente_nome: pedido.cliente.nome,
        cliente_documento: pedido.cliente.cnpj,
        valor_mensal: pedido.valorFormatado,
        vigencia_meses: String(pedido.vigenciaMeses),
      },
      papeis: [
        {
          papelId: PAPEL_CONTRATANTE,
          nome: pedido.cliente.responsavel,
          email: pedido.cliente.email,
          telefone: pedido.cliente.telefone, // exigido pelo fator SMS_OTP
        },
        {
          papelId: PAPEL_CONTRATADA,
          nome: "Carlos Souza",
          email: "carlos@empresa.com.br",
          telefone: "+5511977776666",
        },
      ],
    }),
  });

  const documento = await criar.json();

  if (!criar.ok) {
    // ENTRADA_INVALIDA aqui quase sempre e variavel faltando; o campo
    // "erros" diz qual.
    throw new Error(
      `${documento.codigo}: ${documento.title} ${JSON.stringify(documento.erros ?? [])} ` +
        `(requestId ${documento.requestId})`
    );
  }

  // 2. Enviar, que continua sendo uma chamada separada.
  const enviar = await fetch(`${BASE}/documentos/${documento.id}/enviar`, {
    method: "POST",
    headers: { ...cabecalhos, "Idempotency-Key": `contrato-pedido-${pedido.numero}-enviar` },
  });

  const enviado = await enviar.json();
  if (!enviar.ok) {
    throw new Error(`${enviado.codigo}: ${enviado.title} (requestId ${enviado.requestId})`);
  }
  return enviado;
}

7. Organizar em pastas

Pasta é organização, não permissão: colocar um documento em pasta não restringe quem o vê. Quem vê o quê é papel, e isso está em Autenticação.

Um desenho que funciona bem em integração é uma pasta por período e uma subpasta por tipo, criadas sob demanda pelo próprio código. Assim o acervo não vira uma lista de dez mil documentos na raiz.

// pastas.mjs: garante a arvore "2026/08/Contratos" e devolve o id da folha.
//   ADM_CHAVE=... node pastas.mjs

const CHAVE = process.env.ADM_CHAVE;
const BASE = "https://api.adigitalmax.com.br/v1/sandbox";

const cabecalhos = {
  Authorization: `Bearer ${CHAVE}`,
  "Content-Type": "application/json",
  Accept: "application/json",
};

/**
 * Devolve o id da pasta com este nome sob este pai, criando se faltar.
 * Idempotente por natureza: rodar dez vezes nao cria dez pastas.
 */
async function garantirPasta(nome, paiId = null) {
  const query = new URLSearchParams({ busca: nome, limite: "100" });
  if (paiId) query.set("paiId", paiId);

  const lista = await fetch(`${BASE}/pastas?${query}`, { headers: cabecalhos })
    .then((r) => r.json());

  // A busca e por trecho; a comparacao exata e nossa.
  const existente = lista.itens.find(
    (p) => p.nome === nome && (p.paiId ?? null) === paiId
  );
  if (existente) return existente.id;

  const criada = await fetch(`${BASE}/pastas`, {
    method: "POST",
    headers: cabecalhos,
    body: JSON.stringify({ nome, paiId }),
  });

  if (criada.status === 409) {
    // Outro processo criou entre a leitura e a escrita. Releia.
    return garantirPasta(nome, paiId);
  }

  const corpo = await criada.json();
  if (!criada.ok) throw new Error(`${corpo.codigo}: ${corpo.title}`);
  return corpo.id;
}

async function pastaDoMes(data = new Date(), tipo = "Contratos") {
  const ano = String(data.getFullYear());
  const mes = String(data.getMonth() + 1).padStart(2, "0");

  const idAno = await garantirPasta(ano);
  const idMes = await garantirPasta(mes, idAno);
  return garantirPasta(tipo, idMes);
}

const destino = await pastaDoMes();
console.log("Pasta de destino:", destino);

// Mover um documento existente para la:
// await fetch(`${BASE}/documentos/${docId}/mover`, {
//   method: "POST", headers: cabecalhos,
//   body: JSON.stringify({ pastaId: destino }),
// });

8. Validar a autenticidade de um documento

Esta é a única parte da API que não precisa de credencial, porque quem valida costuma ser exatamente quem não é cliente de ninguém: a contraparte, o cartório, o banco, o juiz.

O endereço é diferente do resto da API. A validação mora no host institucional, em POST https://adigitalmax.com.br/api/v1/validacao/consultar, e não em api.. A página de validação fica no mesmo domínio, então a chamada é de mesma origem e não exige CORS aberto justamente no endpoint mais exposto do sistema.

Há três formas de entrada, e ao menos uma é obrigatória: codigo, hashArquivo ou qrCode. O limite é de 30 por minuto por IP, com rajada de 10.

#!/usr/bin/env bash
# Valida um PDF sem enviar o arquivo: manda so o resumo SHA-256.
# Sem chave de API, de proposito.
#   ./validar.sh ./contrato-assinado.pdf

set -euo pipefail

VALIDACAO="https://adigitalmax.com.br/api/v1/validacao/consultar"
PDF="${1:?informe o caminho do PDF}"

HASH=$(sha256sum "$PDF" | cut -d' ' -f1)
echo "sha256: $HASH"

curl --silent --show-error \
  --request POST "$VALIDACAO" \
  --header "Content-Type: application/json" \
  --data "$(jq -nc --arg h "$HASH" '{hashArquivo:$h}')" \
  | jq '{resultado,
         documento: .documento.nome,
         assinantes: [.signatarios[] | "\(.nome) em \(.assinadoEm)"]}'

# Validar pelo codigo impresso, quando so ha o papel na mao:
#   curl --silent --request POST "$VALIDACAO" \
#     --header "Content-Type: application/json" \
#     --data '{"codigo":"ADM-7K4Q-2M9X-B3TD"}' | jq .

Para o seu usuário final, o caminho mais simples é o link https://adigitalmax.com.br/validar, que já faz tudo isto em uma página pública: aceita código, aceita o PDF calculando o hash no navegador, e lê QR Code. Você não precisa construir tela nenhuma; basta apontar para lá.