---
url: https://developers.zarv.com/api/id-v3/webhook.md
description: >-
  Receba o fim de um enriquecimento do Zarv ID v3 por callback assinado com
  HMAC-SHA256: headers, payload, verificação da assinatura e reentregas.
---

# Webhook de Enriquecimento

## Visão Geral

Quando um enriquecimento não termina dentro da espera síncrona de 25 segundos da criação (a resposta é `202`), a Zarv pode avisar o seu sistema quando ele chegar a `COMPLETED` ou `FAILED`, com um `POST` assinado no endpoint que você indicar. É a alternativa ao polling em `GET /api/v3/enrichments/{id}`.

O callback só é enviado para um enriquecimento cuja criação respondeu `202`. Se a criação respondeu `200`, o resultado já veio na própria resposta e nenhum callback é enviado.

## Configuração

Inclua um objeto `callback` no corpo de `POST /api/v3/enrichments`:

```json
{
  "document": "111.444.777-35",
  "profileId": "8f14e45f-ceea-4e7a-9c1a-2b3c4d5e6f70",
  "callback": {
    "url": "https://sua-empresa.com/webhooks/zarv",
    "headers": {
      "Authorization": "Bearer <token-do-seu-endpoint>",
      "X-Tenant-Id": "my-tenant"
    }
  }
}
```

| Parâmetro | Tipo                   | Obrigatório | Descrição                                                                                                                                                             |
| --------- | ---------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`     | string                 | Sim         | Onde entregar o callback. Só `https`, até 2048 caracteres. Endereço IP literal, `localhost`, `*.internal` e `*.cluster.local` são recusados (`400 invalid_callback`). |
| `headers` | object (string→string) | Não         | Até 20 headers enviados junto do callback, por exemplo para autenticar no seu endpoint.                                                                               |

O callback é sempre um `POST` com `Content-Type: application/json`. `Content-Type`, `X-Zarv-Timestamp` e `X-Zarv-Signature` são reservados e não podem ser definidos em `headers`.

::: info Headers de credencial ficam cifrados
Headers que são credenciais — `Authorization`, `Proxy-Authorization`, `Cookie`, `X-Api-Key`/`Api-Key`/`ApiKey` e qualquer nome que contenha `token`, `secret`, `password`, `passwd`, `apikey`, `auth`, `signature`, `credential` ou `session` — são guardados **cifrados** e só são abertos no momento do envio. Nenhuma resposta da API devolve a configuração do callback.
:::

Um replay idempotente (mesma `Idempotency-Key` e mesmo corpo) de um enriquecimento ainda em andamento rearma o `callback` enviado junto.

## Requisição do Webhook

```http
POST /webhooks/zarv HTTP/1.1
Host: sua-empresa.com
Content-Type: application/json
X-Zarv-Timestamp: 1790600404
X-Zarv-Signature: sha256=5d1f0c3e9a7b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e0f2a4b6c8d0e2f4a6b8c0d
Authorization: Bearer <token-do-seu-endpoint>

{"type":"enrichment.completed","id":"enr_0199a0b2-7c1e-7d4a-9f0e-3b1c2d4e5f60","status":"COMPLETED","outcome":"ok","createdAt":"2026-09-28T13:00:00Z","completedAt":"2026-09-28T13:00:41Z"}
```

### Headers

| Header             | Descrição                                                     |
| ------------------ | ------------------------------------------------------------- |
| `X-Zarv-Timestamp` | Momento do envio, em segundos Unix                            |
| `X-Zarv-Signature` | `sha256=` seguido do HMAC-SHA256 em hexadecimal (veja abaixo) |
| os seus `headers`  | Os headers configurados em `callback.headers`                 |

### Campos do Payload

| Campo         | Tipo           | Descrição                                                              |
| ------------- | -------------- | ---------------------------------------------------------------------- |
| `type`        | string         | Sempre `enrichment.completed`                                          |
| `id`          | string         | O id do enriquecimento                                                 |
| `status`      | string         | `COMPLETED` ou `FAILED`                                                |
| `outcome`     | string | null | `ok` ou `subject_not_found` quando `COMPLETED`; `null` quando `FAILED` |
| `createdAt`   | string         | Criação do enriquecimento (RFC 3339, UTC)                              |
| `completedAt` | string         | Quando chegou ao status terminal (RFC 3339, UTC)                       |

O payload **não traz o documento nem os dados**: busque-os com as suas credenciais em `GET /api/v3/enrichments/{id}?include=data`. Também não traz `metadata`; use o `id` para relacionar o callback ao seu pedido.

## Verificando a Assinatura

Todo callback é assinado com um **segredo de assinatura**. Consulte-o com [`GET /api/v3/callback-secret`](/api/id-v3/getCallbackSecret) e guarde-o no seu cofre de segredos:

```bash
curl "https://services.zarv.com/api/v3/callback-secret" \
  -H "Authorization: Bearer SEU_TOKEN_DE_ACESSO"
# {"secret":"<64 caracteres hexadecimais>"}
```

Para validar um callback:

1. Leia `X-Zarv-Timestamp` e o **corpo bruto** da requisição, exatamente como chegou (antes de qualquer parse de JSON).
2. Calcule `HMAC-SHA256(chave, "{X-Zarv-Timestamp}.{corpo}")`, em que a **chave são os 32 bytes do segredo decodificado de hexadecimal** (não o texto hexadecimal).
3. Compare `sha256=<hex do resultado>` com `X-Zarv-Signature` usando **comparação em tempo constante** (`crypto.timingSafeEqual`, `hmac.Equal`, `hmac.compare_digest`) — nunca `==`.
4. Rejeite o callback se o timestamp estiver a mais de **300 segundos** do seu relógio, para que uma assinatura antiga não possa ser reaproveitada.
5. **Deduplique pelo `id`** do corpo: a entrega é *at-least-once*, e o mesmo enriquecimento pode chegar mais de uma vez.

::: code-group

```js [Node.js]
import crypto from "node:crypto";
import express from "express";

const secret = Buffer.from(process.env.ZARV_CALLBACK_SECRET, "hex");
const app = express();

app.post(
  "/webhooks/zarv",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const timestamp = req.get("X-Zarv-Timestamp") ?? "";
    const signature = req.get("X-Zarv-Signature") ?? "";

    const expected =
      "sha256=" +
      crypto
        .createHmac("sha256", secret)
        .update(`${timestamp}.`)
        .update(req.body) // corpo bruto (Buffer)
        .digest("hex");

    const valid =
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
    const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
    if (!valid || !fresh) return res.status(401).end();

    const event = JSON.parse(req.body);
    res.status(200).end(); // confirme logo; processe em segundo plano
    processOnce(event.id, event); // deduplique pelo id
  },
);
```

```go [Go]
package webhook

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"math"
	"net/http"
	"strconv"
	"time"
)

func Verify(secretHex string, r *http.Request) ([]byte, bool) {
	secret, err := hex.DecodeString(secretHex)
	if err != nil {
		return nil, false
	}
	body, err := io.ReadAll(r.Body) // corpo bruto
	if err != nil {
		return nil, false
	}
	ts := r.Header.Get("X-Zarv-Timestamp")
	sec, err := strconv.ParseInt(ts, 10, 64)
	if err != nil || math.Abs(float64(time.Now().Unix()-sec)) > 300 {
		return nil, false
	}
	mac := hmac.New(sha256.New, secret)
	mac.Write([]byte(ts + "."))
	mac.Write(body)
	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
	return body, hmac.Equal([]byte(expected), []byte(r.Header.Get("X-Zarv-Signature")))
}
```

```py [Python]
import hashlib
import hmac
import time

SECRET = bytes.fromhex(ZARV_CALLBACK_SECRET)

def verify(headers, raw_body: bytes) -> bool:
    timestamp = headers.get("X-Zarv-Timestamp", "")
    signature = headers.get("X-Zarv-Signature", "")
    try:
        if abs(time.time() - int(timestamp)) > 300:
            return False
    except ValueError:
        return False
    digest = hmac.new(SECRET, f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(f"sha256={digest}", signature)
```

:::

## Entrega e Novas Tentativas

* Responda com qualquer `2xx` para confirmar o recebimento. A Zarv espera no máximo **10 segundos** pela resposta: confirme logo e processe em segundo plano.
* Falha de conexão, timeout e respostas `408`, `425`, `429` ou `5xx` são **reenviadas automaticamente**, com intervalo crescente.
* Respostas `3xx` e as demais `4xx` são tratadas como **definitivas** e não são reenviadas. Redirecionamentos não são seguidos: use a URL final.
* Se o seu endpoint ficar fora do ar e o callback não chegar, o resultado continua disponível em `GET /api/v3/enrichments/{id}`. Para pedir um novo envio, fale com o time da Zarv.

## Melhores Práticas

1. Valide a assinatura e o timestamp antes de qualquer processamento.
2. Deduplique pelo `id` — o mesmo callback pode chegar mais de uma vez.
3. Responda `2xx` rapidamente e processe de forma assíncrona.
4. Busque os dados com `GET /api/v3/enrichments/{id}?include=data` depois do callback, com as suas credenciais.
5. Use HTTPS com certificado válido e, se quiser, autentique o seu endpoint com `callback.headers`.
6. Mantenha o polling como plano B: se um callback não chegar, consulte o enriquecimento pelo `id` retornado no `202`.
