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:
{
"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.
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
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 e guarde-o no seu cofre de segredos:
curl "https://services.zarv.com/api/v3/callback-secret" \
-H "Authorization: Bearer SEU_TOKEN_DE_ACESSO"
# {"secret":"<64 caracteres hexadecimais>"}Para validar um callback:
- Leia
X-Zarv-Timestampe o corpo bruto da requisição, exatamente como chegou (antes de qualquer parse de JSON). - 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). - Compare
sha256=<hex do resultado>comX-Zarv-Signatureusando comparação em tempo constante (crypto.timingSafeEqual,hmac.Equal,hmac.compare_digest) — nunca==. - 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.
- Deduplique pelo
iddo corpo: a entrega é at-least-once, e o mesmo enriquecimento pode chegar mais de uma vez.
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
},
);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")))
}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
2xxpara 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,429ou5xxsão reenviadas automaticamente, com intervalo crescente. - Respostas
3xxe as demais4xxsã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
- Valide a assinatura e o timestamp antes de qualquer processamento.
- Deduplique pelo
id— o mesmo callback pode chegar mais de uma vez. - Responda
2xxrapidamente e processe de forma assíncrona. - Busque os dados com
GET /api/v3/enrichments/{id}?include=datadepois do callback, com as suas credenciais. - Use HTTPS com certificado válido e, se quiser, autentique o seu endpoint com
callback.headers. - Mantenha o polling como plano B: se um callback não chegar, consulte o enriquecimento pelo
idretornado no202.