Integre ERPs e sistemas de terceiros para criar, enviar, acompanhar e validar documentos assinados sem passar pelo portal web. Autenticação OAuth 2.0 Client Credentials, respostas em JSON e escrita idempotente.
URL base: https://hashdoc.com.br/api/public/v1
Antes de começar
No portal, abra API e integrações e gere uma credencial. Oclient_secret aparece uma única vez.
Troque as credenciais por um access token (válido por 1 hora) e envie-o no header Authorization: Bearer.
Na criação de documentos e no registro de recibos ERP, envie Idempotency-Key com uma chave estável do seu sistema (nº do pedido, por exemplo).
A credencial está presa a um escopo (pessoal ou empresa): ela só enxerga documentos daquele contexto.
Os exemplos usam dados fictícios e valores abreviados: substitua CPF, CNPJ, UUID, hash e base64 pelos valores reais. Calcule SHA-256 sobre os bytes originais do PDF, antes de codificar em base64.
Regras de integração
Permissões e credenciais
documents:read lista documentos, detalhes e recibos; documents:write cria documentos, cancela documentos e registra recibos; documents:send envia para assinatura; files:read gera URLs de download. Se scope for omitido ao obter o token, são usados os escopos da credencial. Não há refresh token: obtenha outro token ao expirar. A revogação da credencial bloqueia seus tokens.
Guarde client_secret e access_token somente no servidor do ERP, nunca em páginas públicas, aplicativos distribuídos ou logs. A API aceita OPTIONS e CORS, mas isso não torna seguro expor credenciais no navegador.
WhatsApp é opcional, por participante
Somente e-mail: omita send_whatsapp ou envie false. phone sozinho não autoriza mensagens.
E-mail + WhatsApp: informe phone e send_whatsapp=true na mesma parte. O e-mail continua obrigatório. true sem telefone retorna 400 invalid_input.
O celular do cadastro não é usado como alternativa para disparar convites. A escolha é individual, inclusive quando vários participantes pertencem à mesma empresa.
Os convites automáticos saem pela linha (11) 99223-8963. A central (11) 5197-6555 é de atendimento e não é usada para esses disparos.
Este contrato controla o convite e as notificações do documento, não substitui a verificação de identidade necessária para assinar.
Exemplos de participantes (fragmentos do array parties)
Na criação com envio imediato, leia send.sign_links[].sign_url. No envio separado, leia sign_links[].sign_url. Cada item contém party_id e name; o link é individual, vale até a assinatura ou expiração (30 dias) e pode ser enviado pelo próprio ERP. Trate o token como informação confidencial: não publique, não coloque em logs nem compartilhe com outro participante. GET /documents/{id} não retorna esses links. Não há endpoint v1 dedicado para gerá-los novamente.
invites[] usa partyId e ok, não party_id e sent. ok acompanha o resultado do e-mail; whatsapp indica o resultado do disparo de WhatsApp, whatsappPhone o número usado e whatsappReason a causa de ausência/falha. reason/error descrevem falhas do convite. credentialsProvisioned informa criação de acesso e messageId pode aparecer quando o e-mail foi aceito.
invites_ok=true significa que a tarefa de convites terminou sem exceção; não garante sucesso de todos os destinatários. whatsapp=true significa que o provedor aceitou o envio, não que a pessoa recebeu ou leu. Exemplos de whatsappReason: missing_phone, whatsapp_not_configured, invalid_template_link e exception; outros motivos podem vir do provedor. Falha no WhatsApp não impede o e-mail nem desfaz o envio do documento.
warnings[] (na criação e em send) traz avisos que não bloqueiam a operação, com code, party e message. whatsapp_not_requested: o celular foi informado, mas send_whatsapp não é true, então o WhatsApp não será enviado. whatsapp_missing_phone: send_whatsapp é true, mas falta phone. Recomendamos registrar esses avisos no log do seu sistema.
Idempotência e recuperação após falhas
POST /documents: a chave é comparada dentro do contexto pessoal/empresa. Use uma chave única por documento e compartilhe a mesma chave somente nas retentativas daquela criação. external_ref não deduplica. O conteúdo não é comparado: não reutilize a chave para outro PDF ou participantes.
Na repetição, a criação retorna 200, deduped=true, document_id, status="draft" e send=null. Esse status é o formato da resposta de deduplicação, não necessariamente a situação atual. Consulte GET /documents/{id} para conhecer o estado real.
Se send_immediately falhar depois da criação, o rascunho pode já existir. Repetir a criação com a mesma chave (ou o mesmo external_ref) devolve o mesmo documento e conclui o envio se ainda faltar, sem disparar convites em duplicidade. Confira documents:send e saldo antes do envio imediato.
POST /receipts: a chave é por credencial, não por documento. Inclua documento e ocorrência na chave. Na repetição retorna duplicated=true e receipt apenas com id, status, occurred_at e created_at.
/send e DELETE podem ser repetidos com segurança: não reenviam convites nem retornam erro se a operação já foi feita. Não execute tentativas paralelas da mesma operação.
Fluxo recomendado e limites
Obtenha o token, calcule file_sha256 e crie o documento com o PDF e os participantes. Sem arquivo é possível criar um rascunho, mas não há endpoint v1 para anexá-lo depois; prefira criar com arquivo. Se file_id estiver presente, ele tem prioridade sobre file_base64.
Envie uma vez, guarde document_id, sign_links e resultados individuais. Os convites são simultâneos; signing_order é apenas visual.
Acompanhe por consulta de status e pelos eventos disponíveis. Para guardar o documento final, exija status=completed e sealed=true no download; baixe antes de a URL temporária expirar e registre o recibo archived do ERP.
A API cria um documento por requisição, com até 50 participantes. Não há endpoint público v1 de lote, edição de participantes, reenvio, P7S ou registro em cartório. Os limites de 100 PDFs do lote no portal não se aplicam a este endpoint. Não há quota numérica ou limite de arquivo específico informado por este contrato; infraestrutura e armazenamento podem impor restrições. Use chamadas cadenciadas, recuo progressivo em falhas temporárias e não repita erros de validação sem corrigir os dados.
A lista de documentos também pode conter description, updated_at, file_id, sealed_file_id, owner_context_type, owner_user_id e owner_company_id. Campos adicionais podem ser incluídos no futuro: ignore os que sua integração não usa. Datas são ISO 8601; valores ainda não disponíveis podem ser null.
POST
/oauth/token
sem token
Obter access token
Troca client_id e client_secret por um token opaco válido por 1 hora. Aceita credenciais no corpo (JSON ou form-urlencoded) ou no header Basic.
Campo
Tipo
Obrig.
Observação
grant_type
string
não
Envie client_credentials para seguir OAuth 2.0. A implementação aceita omissão; outros valores são recusados.
client_id
string
sim
Gerado na tela API e integrações.
client_secret
string
sim
Mostrado apenas na criação.
scope
string
não
Subconjunto dos escopos do cliente, separado por espaço.
escopo: documents:write (+ documents:send se send_immediately)
Criar documento com signatários
Cria o rascunho, envia o PDF para o cofre (content-addressable) e opcionalmente já dispara os convites em broadcast. Envie o header Idempotency-Key para evitar duplicidade em reprocessamentos do ERP.
Campo
Tipo
Obrig.
Observação
title
string
sim
2 a 200 caracteres.
description
string | null
não
Até 2.000 caracteres.
file_base64
string
não
PDF em base64. Exige file_sha256.
file_sha256
string
não
SHA-256 dos bytes do PDF: exatamente 64 caracteres hexadecimais minúsculos, validado no servidor. Não é o document_hash do envelope.
file_mime_type
string
não
Padrão application/pdf. Envie um PDF em base64 puro, sem prefixo data:.
file_id
uuid
não
Alternativa ao base64: arquivo já existente no cofre.
external_ref
string | null
não
Sua chave de negócio (até 160 caracteres). Não substitui Idempotency-Key.
parties[]
array
sim
De 1 a 50 participantes. Campos detalhados abaixo.
parties[].name
string
sim
Nome de 2 a 160 caracteres.
parties[].cpf
string
sim
CPF com 11 dígitos; aceita pontuação. A consulta detalhada devolve cpf_masked.
parties[].email
string
sim
E-mail válido, obrigatório mesmo quando o WhatsApp estiver habilitado.
parties[].phone
string | null
não
Telefone informado nesta requisição, com DDD. Aceita +, espaços, parênteses e hífen; após normalização deve ter 10 a 13 dígitos. Ex.: 11999998888 ou +55 (11) 99999-8888.
parties[].send_whatsapp
boolean
não
Desligado por padrão. Só true com phone informado autoriza convite por WhatsApp. Não utiliza o telefone do cadastro como alternativa.
parties[].role
string
não
signer (padrão), witness ou approver.
parties[].signing_order
integer
não
1 a 999; padrão: posição no array + 1. Apenas ordem de exibição: não impõe assinatura sequencial.
parties[].representation
string
não
personal (padrão) ou company.
parties[].company_cnpj
string | null
não
CNPJ representado, até 20 caracteres; salvo somente com dígitos.
parties[].company_name
string | null
não
Empresa representada, até 200 caracteres.
parties[].company_role
string | null
não
Cargo do representante, até 120 caracteres.
send_immediately
boolean
não
Padrão false. true dispara convites e exige documents:send. Envie o PDF ou file_id para poder enviar.
Coloca o rascunho em assinatura e dispara convites simultaneamente. WhatsApp segue send_whatsapp + phone registrados na criação; não há corpo para alterar contatos. Exige saldo de envelopes. É seguro repetir: se o documento já estiver em assinatura, a resposta é 200 com already_sent: true e os links atuais, sem novo disparo nem nova cobrança. Depois que o documento entra em assinatura, falhas internas secundárias não viram erro na resposta.
Requisição
curl -X POST https://hashdoc.com.br/api/public/v1/documents/7d0c1f4e-.../send \
-H "Authorization: Bearer $TOKEN"
Cancela documento do contexto da credencial com status draft ou awaiting_signatures. Marca como cancelled, invalida a assinatura pendente e registra a integração e o motivo na auditoria; não apaga o arquivo nem o histórico. Documentos completed ou expired retornam 409 conflict. Repetir após cancelar retorna 200 com "already_cancelled": true.
Campo
Tipo
Obrig.
Observação
reason
string (query)
não
Motivo na URL, codificado; limitado a 500 caracteres. Não envie corpo JSON no DELETE.
O ERP informa que recebeu, está processando ou arquivou o documento. O recibo aparece no portal e não altera o status das assinaturas. Não é um webhook de saída. Use Idempotency-Key exclusivo por documento e ocorrência.
Campo
Tipo
Obrig.
Observação
status
string
não
received (padrão), processing, archived, rejected ou error.
system
string
não
Nome do sistema, limitado a 120 caracteres; padrão: nome da credencial.
external_ref
string
não
Referência do ERP, limitada a 200 caracteres; padrão: referência do documento.
message
string
não
Mensagem limitada a 500 caracteres.
occurred_at
string
não
Data/hora ISO 8601. Ausente ou inválida usa o horário atual. O corpo enviado também é guardado no recibo: não inclua segredos.
Retorna os últimos 50 recibos do documento, do mais recente para o mais antigo. Sem parâmetros de paginação. receipts é um array vazio quando ainda não há confirmações.
Retorna JSON com URL temporária, não o PDF diretamente. Prioriza o arquivo selado; quando ainda não existe, pode retornar o original. Para arquivar como assinado, confira status=completed e sealed=true. Se sealed=false, aguarde e consulte novamente. Não há download P7S nesta API v1.
Campo
Tipo
Obrig.
Observação
expires_in
number
não
Validade da URL em segundos: 30 a 3600 (padrão 300).
Endpoint aberto (não exige token). Consulta os mesmos dados do validador público pelo document_hash do envelope: 64 caracteres hexadecimais minúsculos. Não confunda com file_sha256. found=true indica que o registro foi encontrado; não substitui validação criptográfica ICP-Brasil do PDF.
Em vez de consultar o status em loop, cadastre um endpoint HTTPS na tela API e integrações e receba os eventos do ciclo de assinatura assim que acontecem. O segredo de assinatura (whsec_…) é exibido uma única vez e pode ser rotacionado a qualquer momento.
Os eventos abaixo são reconhecidos, mas criação, envio e descarte pela API v1 não emitem esses eventos diretamente. Não dependa de document.created, document.sent ou document.cancelled para confirmar essas chamadas: use a resposta e a consulta de status. document.completed é emitido depois do fechamento físico do PDF no fluxo de conclusão. Configure os eventos no portal; não há CRUD de webhooks na API v1.
Toda conclusão (document.completed) traz em data.signed_document o link temporário para baixar o contrato assinado — um link novo a cada tentativa, válido por 7 dias, com fallback em api_download_path. O identificador também vem em event.id, igual ao id da raiz, para receptores que leem o evento por esse campo.
Evento
Quando dispara
document.created
Um documento foi criado no fluxo que emite este evento; não use como confirmação da criação via API, cuja resposta retorna document_id.
document.sent
Convites de assinatura foram disparados (broadcast).
document.signed
Uma parte assinou; o payload traz o total assinado/pendente.
document.completed
Todos assinaram e o PDF selado já está disponível. O payload traz data.signed_document com o link temporário do contrato assinado (novo a cada tentativa, válido por 7 dias).
document.cancelled
O cancelamento foi emitido pelo fluxo do portal; o cancelamento DELETE da API não emite este evento (consulte GET /documents/{id}).
import crypto from "node:crypto";
app.post("/hashdoc/webhook", express.raw({ type: "*/*" }), (req, res) => {
const header = req.get("X-HashDoc-Signature") ?? ""; // t=...,v1=...
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=")),
);
const body = req.body.toString("utf8");
// Rejeite eventos com mais de 5 minutos (proteção contra replay)
const timestamp = Number(parts.t);
const age = Math.abs(Date.now() / 1000 - timestamp);
if (!/^\d+$/.test(parts.t ?? "") || !Number.isFinite(timestamp) || age > 300)
return res.sendStatus(400);
if (!/^[0-9a-f]{64}$/.test(parts.v1 ?? "")) return res.sendStatus(401);
const secret = process.env.HASHDOC_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(503);
const expected = crypto
.createHmac("sha256", secret)
.update(parts.t + "." + body)
.digest("hex");
const ok =
crypto.timingSafeEqual(Buffer.from(parts.v1, "hex"), Buffer.from(expected, "hex"));
if (!ok) return res.sendStatus(401);
let event;
try { event = JSON.parse(body); } catch { return res.sendStatus(400); }
// Grave em uma fila durável, deduplicando event.id, antes de confirmar.
// Implemente enfileirarEvento no seu ERP; responda 5xx se a gravação falhar.
enfileirarEvento(event)
.then(() => res.sendStatus(200))
.catch(() => res.sendStatus(500));
});
Entrega e retentativas
Considera-se sucesso qualquer resposta 2xx em até 10 segundos.
São até 6 tentativas automáticas no total: a inicial e retentativas após 1, 5, 15, 60 e 180 minutos. Os intervalos são contados após cada falha e a execução depende do agendamento. Após a sexta falha, a entrega fica exhausted.
O mesmo evento pode chegar mais de uma vez: use id do evento como chave de deduplicação no seu ERP.
Em retentativas, id permanece igual; attempt, timestamp, created_at e assinatura são renovados. Não presuma ordem de chegada. Valide sobre o corpo bruto, antes de qualquer conversão JSON; o exemplo usa Node.js no servidor do integrador.
O histórico de entregas, o reenvio manual e o disparo de teste (ping) ficam na tela API e integrações.
Erros
Todo erro devolve o mesmo formato: { "error": "codigo", "message": "descrição" }
HTTP
error
Quando acontece
400
invalid_input
Campo obrigatório ausente ou fora do formato esperado.
400
invalid_json
Corpo enviado não é um JSON válido.
400
invalid_scope
Escopo solicitado no token não foi concedido ao cliente.
400
unsupported_grant_type
Use grant_type=client_credentials.
400
invalid_status
Status do recibo ERP fora dos valores aceitos.
403
forbidden
Documento fora do contexto autorizado (por exemplo, em recibos ERP).
401
unauthorized
Header Authorization ausente ou malformado.
401
invalid_token
Token inválido, expirado ou revogado.
401
invalid_client
Credenciais incorretas ou credencial revogada.
403
insufficient_scope
A credencial não tem a permissão exigida pela rota. A mensagem informa qual permissão falta, por exemplo files:read para download. Depois de incluí-la, gere um novo token.
404
not_found
Documento fora do escopo da credencial ou inexistente.
409
conflict
Descarte recusado: documento não está em draft ou já possui assinatura.
500
failed / server_error
Falha interna. Consulte o estado antes de repetir; use a mesma chave na criação e em recibos.