> ## Documentation Index
> Fetch the complete documentation index at: https://docs-platform.services-valid.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook de Resultado

> Receba o resultado da análise assim que ela é concluída

Quando a análise de um documento termina, a plataforma faz um `POST` na URL cadastrada no seu projeto, com o resultado completo. É o caminho oposto dos outros endpoints desta seção: aqui **o seu serviço é quem recebe a chamada**.

O disparo acontece uma única vez por documento, assim que ele chega em `COMPLETED`. Sem URL cadastrada, nada é enviado e o resultado fica disponível apenas por consulta em [Consultar Documento](/plataforma/documentoscopia/api/get-document-by-id).

## Habilitando

Grave os parâmetros abaixo com [Criar ou Atualizar Parâmetro](/plataforma/documentoscopia/api/post-parameter):

| Chave                            | Efeito                                                                 |
| :------------------------------- | :--------------------------------------------------------------------- |
| `PARAMETER_WEBHOOK_URL`          | URL que recebe o `POST`. Sem ela, nenhum disparo acontece              |
| `PARAMETER_WEBHOOK_HEADER_KEY`   | Nome de um header de autenticação enviado na chamada                   |
| `PARAMETER_WEBHOOK_HEADER_VALUE` | Valor desse header                                                     |
| `PARAMETER_WEBHOOK_HMAC_SECRET`  | Segredo que assina a chamada, habilitando o header `x-valid-signature` |

<Note>
  `PARAMETER_WEBHOOK_HEADER_KEY` e `PARAMETER_WEBHOOK_HEADER_VALUE` só são
  enviados quando os dois estão preenchidos. Se apenas um estiver definido,
  nenhum header extra é adicionado à chamada.
</Note>

## Headers da chamada

<ParamField header="Content-Type" type="string">
  Sempre `application/json`.
</ParamField>

<ParamField header="x-valid-signature" type="string">
  Assinatura HMAC-SHA256 do corpo. Presente apenas quando `PARAMETER_WEBHOOK_HMAC_SECRET` está cadastrado.

  **Exemplo**: `t=1758499200,v1=43f80a7f384c4ce59a0357688a554eef55496d52c3f7fd8e63849c82a531063c`
</ParamField>

## Corpo

<ResponseField name="version" type="string">
  Versão do contrato do evento.

  **Exemplo**: `1.0.0`
</ResponseField>

<ResponseField name="env" type="string">
  Ambiente que originou o evento.

  **Exemplo**: `production`
</ResponseField>

<ResponseField name="event.id" type="string">
  Identificador desta entrega. Muda a cada envio, inclusive para o mesmo documento.

  **Exemplo**: `9f1c2e4a-3b7d-4c6e-8a1f-2d5b7c9e0a3f`
</ResponseField>

<ResponseField name="event.type" type="string">
  Origem do evento: `docs-gateway.document.finished` para análise de autenticidade documental, `docs-gateway.ocr.finished` para extração de dados.

  **Exemplo**: `docs-gateway.document.finished`
</ResponseField>

<ResponseField name="event.causedBy" type="string">
  Origem da ação que gerou o evento.

  **Exemplo**: `system.action`
</ResponseField>

<ResponseField name="event.when" type="datetime">
  Data e hora em que a análise foi concluída.

  **Exemplo**: `2026-06-01T12:05:00.000Z`
</ResponseField>

<ResponseField name="event.data" type="object">
  O documento com o resultado, no mesmo formato da resposta de [Consultar Documento](/plataforma/documentoscopia/api/get-document-by-id) — incluindo o detalhamento por regra em `result.raw_response.penalidades`.
</ResponseField>

<ResponseField name="metadata.clientId" type="string">
  O valor que você enviou no header `x-client-id` no momento do envio, ou `null` se não enviou. Use-o para correlacionar o resultado com o registro do seu lado.

  **Exemplo**: `seu-identificador`
</ResponseField>

<Note>
  Dados de biometria facial presentes na consulta não são enviados no webhook.
  Se você depende deles, use [Consultar Documento](/plataforma/documentoscopia/api/get-document-by-id).
</Note>

<ResponseExample>
  ```json Aprovado theme={"theme":"catppuccin-latte"}
  {
    "version": "1.0.0",
    "env": "production",
    "event": {
      "id": "9f1c2e4a-3b7d-4c6e-8a1f-2d5b7c9e0a3f",
      "type": "docs-gateway.document.finished",
      "causedBy": "system.action",
      "when": "2026-06-01T12:05:00.000Z",
      "data": {
        "id": "123e4567-e89b-12d3-a456-426614174000",
        "organizationId": "123e4567-e89b-12d3-a456-426614174001",
        "projectId": "123e4567-e89b-12d3-a456-426614174002",
        "productId": "123e4567-e89b-12d3-a456-426614174003",
        "cpf": "12345678909",
        "status": "COMPLETED",
        "type": "docs",
        "createdAt": "2026-06-01T12:00:00.000Z",
        "updatedAt": "2026-06-01T12:05:00.000Z",
        "sentAt": "2026-06-01T12:00:01.000Z",
        "sentToBackofficeAt": "2026-06-01T12:00:02.000Z",
        "finishedAt": "2026-06-01T12:05:00.000Z",
        "errorMessage": null,
        "errorCode": null,
        "backofficeBatchId": "229462",
        "requestId": "7d7a6f2b-8d9f-4f55-9a2d-ec8e7b4f1d0c",
        "name": "JOAO DA SILVA",
        "workflowPresetAlias": "documentoscopia_auto_base_face_15min",
        "result": {
          "id": "223e4567-e89b-12d3-a456-426614174001",
          "documentId": "123e4567-e89b-12d3-a456-426614174000",
          "status": "APPROVED",
          "confidenceScore": 98.5,
          "raw_response": {
            "cpf": "12345678909",
            "tipo_documento": "RG",
            "indice_avaliacao_autenticidade": "98.5",
            "penalidades": [],
            "codigo_rejeicao": [],
            "checklist": []
          },
          "processedBy": "backoffice",
          "createdAt": "2026-06-01T12:05:00.000Z"
        }
      }
    },
    "metadata": {
      "clientId": "seu-identificador"
    }
  }
  ```

  ```json Reprovado theme={"theme":"catppuccin-latte"}
  {
    "version": "1.0.0",
    "env": "production",
    "event": {
      "id": "5c8b1d3e-9a2f-4e7b-8c1d-3f6a9b2e5d0c",
      "type": "docs-gateway.document.finished",
      "causedBy": "system.action",
      "when": "2026-06-01T12:05:00.000Z",
      "data": {
        "id": "123e4567-e89b-12d3-a456-426614174000",
        "cpf": "12345678909",
        "status": "COMPLETED",
        "type": "docs",
        "finishedAt": "2026-06-01T12:05:00.000Z",
        "name": "JOAO DA SILVA",
        "workflowPresetAlias": "documentoscopia_auto_base_face_15min",
        "result": {
          "status": "REJECTED",
          "confidenceScore": 0,
          "raw_response": {
            "cpf": "12345678909",
            "tipo_documento": "RG",
            "indice_avaliacao_autenticidade": "0",
            "penalidades": [
              {
                "rule": "ocr_01",
                "desc": "(OCR_01) CPF obtido no OCR do documento (98765432100) é diferente de nulo e divergente com CPF informado (12345678909)",
                "data": "01/06/2026 12:04:59",
                "type": 1,
                "score": 100
              }
            ],
            "codigo_rejeicao": [],
            "checklist": []
          },
          "processedBy": "backoffice",
          "createdAt": "2026-06-01T12:05:00.000Z"
        }
      }
    },
    "metadata": {
      "clientId": null
    }
  }
  ```
</ResponseExample>

## Validando a assinatura

Com `PARAMETER_WEBHOOK_HMAC_SECRET` cadastrado, cada chamada leva o header `x-valid-signature`:

```
x-valid-signature: t=1758499200,v1=43f80a7f384c4ce59a0357688a554eef55496d52c3f7fd8e63849c82a531063c
```

| Parte | O que é                                                               |
| :---- | :-------------------------------------------------------------------- |
| `t`   | Instante do envio, em segundos Unix                                   |
| `v1`  | HMAC-SHA256 em hexadecimal de `<t>.<corpo cru>`, usando o seu segredo |

Validar a assinatura confirma que a chamada partiu de quem conhece o segredo e que o corpo não foi alterado no caminho. O segredo nunca trafega na requisição.

<Steps>
  <Step title="Extraia t e v1 do header">
    O header pode trazer mais de um `v1`. Basta um deles conferir.
  </Step>

  <Step title="Rejeite entregas antigas">
    Descarte quando a diferença entre o horário atual e `t` passar da sua tolerância. Recomendamos 300 segundos.
  </Step>

  <Step title="Recalcule o HMAC">
    Use o **corpo cru** da requisição, antes de qualquer conversão para objeto.
  </Step>

  <Step title="Compare em tempo constante">
    Use uma comparação segura, como `timingSafeEqual`, em vez de igualdade simples.
  </Step>
</Steps>

```javascript theme={"theme":"catppuccin-latte"}
const crypto = require('node:crypto');

function assinaturaValida(header, corpoCru, segredo) {
  if (!header) return false;

  const partes = header.split(',').map((parte) => parte.trim());
  const timestamp = partes.find((parte) => parte.startsWith('t='))?.slice(2);
  const assinaturas = partes.filter((parte) => parte.startsWith('v1=')).map((parte) => parte.slice(3));

  if (!timestamp || assinaturas.length === 0) return false;

  const idade = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!Number.isFinite(idade) || idade > 300) return false;

  const esperada = crypto
    .createHmac('sha256', segredo)
    .update(`${timestamp}.${corpoCru}`, 'utf8')
    .digest('hex');
  const esperadaBuffer = Buffer.from(esperada, 'utf8');

  return assinaturas.some((candidata) => {
    const candidataBuffer = Buffer.from(candidata, 'utf8');
    return (
      candidataBuffer.length === esperadaBuffer.length &&
      crypto.timingSafeEqual(candidataBuffer, esperadaBuffer)
    );
  });
}
```

<Warning>
  Valide sempre sobre o corpo cru. Se o seu servidor converter o JSON em objeto
  e você gerar o texto de novo para calcular o HMAC, a ordem das chaves e o
  espaçamento podem mudar, e a assinatura não confere. Em Express, use
  `express.raw()` na rota do webhook, não `express.json()`.
</Warning>

## O que o seu endpoint deve responder

Responda com um status HTTP de sucesso assim que receber a chamada. O processamento do seu lado pode seguir de forma assíncrona.

<Warning>
  A entrega não é repetida em caso de falha. Se o seu endpoint estiver
  indisponível ou responder erro, recupere o resultado pela consulta ao
  `documentId`.
</Warning>

<Tip>
  Para descartar entregas repetidas, use `event.data.id`, que identifica o
  documento. O `event.id` muda a cada entrega e não serve para isso.
</Tip>
