API pública v1

Documentação da API do HashDoc

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

  1. No portal, abra API e integrações e gere uma credencial. Oclient_secret aparece uma única vez.
  2. Troque as credenciais por um access token (válido por 1 hora) e envie-o no header Authorization: Bearer.
  3. 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).
  4. A credencial está presa a um escopo (pessoal ou empresa): ela só enxerga documentos daquele contexto.
  5. 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)
[
  { "name": "Maria Souza", "cpf": "52998224725", "email": "maria@example.com",
    "phone": "11999998888", "send_whatsapp": true },
  { "name": "João Silva", "cpf": "11144477735", "email": "joao@example.com",
    "phone": "11988887777", "send_whatsapp": false }
]

Links diretos e resultado de cada canal

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

  1. 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.
  2. Envie uma vez, guarde document_id, sign_links e resultados individuais. Os convites são simultâneos; signing_order é apenas visual.
  3. 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.

CampoTipoObrig.Observação
grant_typestringnãoEnvie client_credentials para seguir OAuth 2.0. A implementação aceita omissão; outros valores são recusados.
client_idstringsimGerado na tela API e integrações.
client_secretstringsimMostrado apenas na criação.
scopestringnãoSubconjunto dos escopos do cliente, separado por espaço.
Requisição
curl -X POST https://hashdoc.com.br/api/public/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "hdc_9f2b...",
    "client_secret": "s3cr3t...",
    "scope": "documents:read documents:write documents:send files:read"
  }'
Resposta — 200 OK
{
  "access_token": "hd_kJ8x2p...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "documents:read documents:write documents:send files:read"
}
GET
/documents
escopo: documents:read

Listar documentos

Lista os documentos do escopo (pessoal ou empresa) vinculado à credencial, do mais recente para o mais antigo.

CampoTipoObrig.Observação
statusstringnãodraft, awaiting_signatures (aceita também sent ou pending), completed, cancelled ou expired. Valor desconhecido retorna 400.
limitnumbernãoUse inteiro de 1 a 200. Padrão 50; valores acima de 200 são limitados a 200.
offsetnumbernãoInteiro não negativo. Padrão 0. Avance pelo limit; não há total ou cursor no retorno.
Requisição
curl "https://hashdoc.com.br/api/public/v1/documents?status=awaiting_signatures&limit=20" \
  -H "Authorization: Bearer $TOKEN"
Resposta — 200 OK
{
  "documents": [
    {
      "id": "7d0c1f4e-2a55-4a2f-9a1c-8f6f2f1b90aa",
      "title": "Contrato 4711",
      "description": null,
      "status": "awaiting_signatures",
      "document_hash": "9b1c...e4",
      "external_ref": "4711",
      "created_at": "2026-08-31T18:04:11.220Z",
      "updated_at": "2026-08-31T18:06:02.001Z",
      "sealed_at": null,
      "file_id": "9bff32a0-a66c-4f38-a6ca-40ec79fa1c89",
      "sealed_file_id": null,
      "owner_context_type": "company",
      "owner_user_id": null,
      "owner_company_id": "65e7f68c-45c1-4958-bd12-2af2d4b83d0e"
    }
  ],
  "limit": 20,
  "offset": 0
}
POST
/documents
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.

CampoTipoObrig.Observação
titlestringsim2 a 200 caracteres.
descriptionstring | nullnãoAté 2.000 caracteres.
file_base64stringnãoPDF em base64. Exige file_sha256.
file_sha256stringnãoSHA-256 dos bytes do PDF: exatamente 64 caracteres hexadecimais minúsculos, validado no servidor. Não é o document_hash do envelope.
file_mime_typestringnãoPadrão application/pdf. Envie um PDF em base64 puro, sem prefixo data:.
file_iduuidnãoAlternativa ao base64: arquivo já existente no cofre.
external_refstring | nullnãoSua chave de negócio (até 160 caracteres). Não substitui Idempotency-Key.
parties[]arraysimDe 1 a 50 participantes. Campos detalhados abaixo.
parties[].namestringsimNome de 2 a 160 caracteres.
parties[].cpfstringsimCPF com 11 dígitos; aceita pontuação. A consulta detalhada devolve cpf_masked.
parties[].emailstringsimE-mail válido, obrigatório mesmo quando o WhatsApp estiver habilitado.
parties[].phonestring | nullnãoTelefone 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_whatsappbooleannãoDesligado por padrão. Só true com phone informado autoriza convite por WhatsApp. Não utiliza o telefone do cadastro como alternativa.
parties[].rolestringnãosigner (padrão), witness ou approver.
parties[].signing_orderintegernão1 a 999; padrão: posição no array + 1. Apenas ordem de exibição: não impõe assinatura sequencial.
parties[].representationstringnãopersonal (padrão) ou company.
parties[].company_cnpjstring | nullnãoCNPJ representado, até 20 caracteres; salvo somente com dígitos.
parties[].company_namestring | nullnãoEmpresa representada, até 200 caracteres.
parties[].company_rolestring | nullnãoCargo do representante, até 120 caracteres.
send_immediatelybooleannãoPadrão false. true dispara convites e exige documents:send. Envie o PDF ou file_id para poder enviar.
Requisição
curl -X POST https://hashdoc.com.br/api/public/v1/documents \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: pedido-4711" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Contrato 4711",
    "external_ref": "4711",
    "file_base64": "JVBERi0xLjcKJc...",
    "file_sha256": "<sha256-de-64-caracteres-hexadecimais-minusculos>",
    "parties": [
      {
        "name": "Maria Souza",
        "cpf": "00000000000",
        "email": "maria@exemplo.com.br",
        "phone": "11999998888",
        "send_whatsapp": true,
        "role": "signer",
        "representation": "company",
        "company_cnpj": "00000000000191",
        "company_name": "Exemplo Ltda",
        "company_role": "Diretora"
      }
    ],
    "send_immediately": true
  }'
Resposta — 201 Created (200 OK quando deduped=true)
{
  "document_id": "7d0c1f4e-2a55-4a2f-9a1c-8f6f2f1b90aa",
  "deduped": false,
  "status": "awaiting_signatures",
  "send": {
    "document_id": "7d0c1f4e-2a55-4a2f-9a1c-8f6f2f1b90aa",
    "document_hash": "9b1c8d...e4",
    "invites": [
      { "partyId": "1f3e...", "email": "maria@exemplo.com.br", "ok": true,
        "credentialsProvisioned": false, "whatsapp": true,
        "whatsappPhone": "11999998888", "whatsappReason": null }
    ],
    "invites_ok": true,
    "sign_links": [
      { "party_id": "1f3e...", "name": "Maria Souza",
        "sign_url": "https://hashdoc.com.br/assinar/<token-do-signatario>" }
    ]
  }
}
GET
/documents/{id}
escopo: documents:read

Consultar status do documento

Retorna o status, o hash e o andamento das assinaturas. O CPF dos signatários sempre volta mascarado.

Requisição
curl https://hashdoc.com.br/api/public/v1/documents/7d0c1f4e-2a55-4a2f-9a1c-8f6f2f1b90aa \
  -H "Authorization: Bearer $TOKEN"
Resposta — 200 OK
{
  "id": "7d0c1f4e-2a55-4a2f-9a1c-8f6f2f1b90aa",
  "title": "Contrato 4711",
  "description": null,
  "status": "awaiting_signatures",
  "document_hash": "9b1c8d...e4",
  "external_ref": "4711",
  "created_at": "2026-08-31T18:04:11.220Z",
  "updated_at": "2026-08-31T18:06:02.001Z",
  "sealed_at": null,
  "signatures": { "total": 1, "signed": 1, "pending": 0 },
  "parties": [
    {
      "id": "1f3e...",
      "name": "Maria Souza",
      "email": "maria@exemplo.com.br",
      "phone": "11999998888",
      "cpf_masked": "000.***.***-00",
      "role": "signer",
      "signing_order": 1,
      "representation": "company",
      "company_cnpj": "00000000000191",
      "company_name": "Exemplo Ltda",
      "signed_at": "2026-08-31T18:20:44.910Z"
    }
  ]
}
POST
/documents/{id}/send
escopo: documents:send

Enviar para assinatura (broadcast)

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"
Resposta — 200 OK
{
  "status": "awaiting_signatures",
  "document_id": "7d0c1f4e-2a55-4a2f-9a1c-8f6f2f1b90aa",
  "document_hash": "9b1c8d...e4",
  "invites": [
    { "partyId": "1f3e...", "email": "maria@exemplo.com.br", "ok": true,
      "credentialsProvisioned": false, "whatsapp": true,
      "whatsappPhone": "11999998888", "whatsappReason": null },
    { "partyId": "2a7d...", "email": "joao@exemplo.com.br", "ok": true,
      "credentialsProvisioned": false, "whatsapp": false,
      "whatsappPhone": null, "whatsappReason": "missing_phone" }
  ],
  "invites_ok": true,
  "sign_links": [
    { "party_id": "1f3e...", "name": "Maria Souza", "sign_url": "https://hashdoc.com.br/assinar/<token-maria>" },
    { "party_id": "2a7d...", "name": "João Silva", "sign_url": "https://hashdoc.com.br/assinar/<token-joao>" }
  ]
}
DELETE
/documents/{id}
escopo: documents:write

Cancelar documento (rascunho ou em assinatura)

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.

CampoTipoObrig.Observação
reasonstring (query)nãoMotivo na URL, codificado; limitado a 500 caracteres. Não envie corpo JSON no DELETE.
Requisição
curl -X DELETE "https://hashdoc.com.br/api/public/v1/documents/$DOCUMENT_ID?reason=Teste%20de%20integracao" \
  -H "Authorization: Bearer $TOKEN"
Resposta — 200 OK
{
  "document_id": "7d0c1f4e-2a55-4a2f-9a1c-8f6f2f1b90aa",
  "status": "cancelled"
}
POST
/documents/{id}/receipts
escopo: documents:write

Registrar confirmação do ERP

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.

CampoTipoObrig.Observação
statusstringnãoreceived (padrão), processing, archived, rejected ou error.
systemstringnãoNome do sistema, limitado a 120 caracteres; padrão: nome da credencial.
external_refstringnãoReferência do ERP, limitada a 200 caracteres; padrão: referência do documento.
messagestringnãoMensagem limitada a 500 caracteres.
occurred_atstringnãoData/hora ISO 8601. Ausente ou inválida usa o horário atual. O corpo enviado também é guardado no recibo: não inclua segredos.
Requisição
curl -X POST "https://hashdoc.com.br/api/public/v1/documents/$DOCUMENT_ID/receipts" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: contrato-4711-arquivado-1" \
  -H "Content-Type: application/json" \
  -d '{"status":"archived","system":"ERP","external_ref":"4711","message":"PDF assinado arquivado","occurred_at":"2026-10-30T02:00:00Z"}'
Resposta — 201 Created; 200 OK na repetição (duplicated=true)
{
  "receipt": {
    "id": "8f032ee1-7c04-4b6a-8015-d543b24f7fb0",
    "status": "archived",
    "system_name": "ERP",
    "external_ref": "4711",
    "message": "PDF assinado arquivado",
    "occurred_at": "2026-10-30T02:00:00.000Z",
    "created_at": "2026-10-30T02:00:01.000Z"
  }
}
GET
/documents/{id}/receipts
escopo: documents:read

Consultar confirmações do ERP

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.

Requisição
curl "https://hashdoc.com.br/api/public/v1/documents/$DOCUMENT_ID/receipts" \
  -H "Authorization: Bearer $TOKEN"
Resposta — 200 OK
{
  "receipts": [{
    "id": "8f032ee1-7c04-4b6a-8015-d543b24f7fb0",
    "status": "archived", "system_name": "ERP", "external_ref": "4711",
    "message": "PDF assinado arquivado",
    "occurred_at": "2026-10-30T02:00:00.000Z",
    "created_at": "2026-10-30T02:00:01.000Z"
  }]
}
GET
/documents/{id}/download
escopo: files:read

Baixar o PDF

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.

CampoTipoObrig.Observação
expires_innumbernãoValidade da URL em segundos: 30 a 3600 (padrão 300).
Requisição
curl "https://hashdoc.com.br/api/public/v1/documents/7d0c1f4e-.../download?expires_in=600" \
  -H "Authorization: Bearer $TOKEN"
Resposta — 200 OK
{
  "url": "https://.../vault/9b1c8d...e4.pdf?token=...",
  "expires_in": 600,
  "sealed": true,
  "status": "completed"
}
GET
/validate/{hash}
sem token

Validar autenticidade por hash

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.

Requisição
curl https://hashdoc.com.br/api/public/v1/validate/9b1c8d5f0a...e4
Resposta — 200 OK (404 quando found=false)
{
  "found": true,
  "id": "7d0c1f4e-2a55-4a2f-9a1c-8f6f2f1b90aa",
  "title": "Contrato 4711",
  "status": "completed",
  "document_hash": "9b1c8d5f0a...e4",
  "sealed_at": "2026-08-31T19:02:10.441Z",
  "created_at": "2026-08-31T18:04:11.220Z",
  "owner_context_type": "company",
  "owner_name": "Exemplo Ltda",
  "parties": [
    { "name": "Maria Souza", "cpf_masked": "000.***.***-00",
      "representation_type": "company", "represented_company_name": "Exemplo Ltda",
      "represented_company_cnpj": "00000000000191", "represented_company_role": "Diretora",
      "signing_order": 1, "signed_at": "2026-08-31T18:20:44.910Z" }
  ],
  "events": [
    { "signed_at": "2026-08-31T18:20:44.910Z", "occurred_at": "2026-08-31T18:20:44.910Z",
      "event_type": "signature", "auth_method": "safeid", "ip": "192.0.2.1", "party_name": "Maria Souza" }
  ]
}
eventos

Webhooks assinados (HMAC-SHA256)

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.

EventoQuando dispara
document.createdUm 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.sentConvites de assinatura foram disparados (broadcast).
document.signedUma parte assinou; o payload traz o total assinado/pendente.
document.completedTodos 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.cancelledO cancelamento foi emitido pelo fluxo do portal; o cancelamento DELETE da API não emite este evento (consulte GET /documents/{id}).
pingDisparo manual de teste feito pelo portal.
Requisição enviada ao seu endpoint
POST /hashdoc/webhook HTTP/1.1
Content-Type: application/json
User-Agent: HashDoc-Webhooks/1
X-HashDoc-Event: document.completed
X-HashDoc-Delivery: 6b0f...c1
X-HashDoc-Timestamp: 1756672931
X-HashDoc-Signature: t=1756672931,v1=6f6a...9d

{
  "id": "6b0f...c1",
  "type": "document.completed",
  "created_at": "2026-08-31T21:22:11.000Z",
  "attempt": 1,
  "event": {
    "id": "6b0f...c1",
    "type": "document.completed",
    "created_at": "2026-08-31T21:22:11.000Z"
  },
  "data": {
    "document": {
      "id": "7e2c...aa",
      "title": "Contrato 4711",
      "status": "completed",
      "document_hash": "9b1c8d5f0a...e4",
      "external_ref": "4711",
      "sealed_at": "2026-08-31T21:22:09.880Z"
    },
    "signatures": { "total": 2, "signed": 2, "pending": 0 },
    "parties": [
      { "id": "a1...", "name": "Maria Souza", "email": "maria@ex.com",
        "signed_at": "2026-08-31T21:22:08.100Z" }
    ],
    "signed_document": {
      "available": true,
      "sealed": true,
      "download_url": "https://…/assinado-4711.pdf?token=…",
      "expires_at": "2026-09-07T21:22:11.000Z",
      "api_download_path": "/api/public/v1/documents/7e2c...aa/download"
    }
  }
}
Validação da assinatura (Node.js)
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" }

HTTPerrorQuando acontece
400invalid_inputCampo obrigatório ausente ou fora do formato esperado.
400invalid_jsonCorpo enviado não é um JSON válido.
400invalid_scopeEscopo solicitado no token não foi concedido ao cliente.
400unsupported_grant_typeUse grant_type=client_credentials.
400invalid_statusStatus do recibo ERP fora dos valores aceitos.
403forbiddenDocumento fora do contexto autorizado (por exemplo, em recibos ERP).
401unauthorizedHeader Authorization ausente ou malformado.
401invalid_tokenToken inválido, expirado ou revogado.
401invalid_clientCredenciais incorretas ou credencial revogada.
403insufficient_scopeA 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.
404not_foundDocumento fora do escopo da credencial ou inexistente.
409conflictDescarte recusado: documento não está em draft ou já possui assinatura.
500failed / server_errorFalha interna. Consulte o estado antes de repetir; use a mesma chave na criação e em recibos.