Webhooks
Eventos assinados com HMAC-SHA256, política de reentrega e um simulador de payload.
Em vez de perguntar de tempos em tempos se a consulta ficou pronta, você registra uma URL sua e a ZapCar avisa quando o evento acontece. É o caminho recomendado para volumes maiores e para o CRLV e a ATPV-e, que podem levar minutos: economiza requisições e chega antes.
Configurar um endpoint
Passo 1: Cadastre a URL
No Portal do Cliente, em Conta › Integrações, aba Webhooks. A URL precisa ser HTTPS e pública: endereços de rede interna (localhost, 10.x, 192.168.x, 169.254.x…) são recusados no cadastro e de novo antes de cada entrega.Passo 2: Escolha os eventos
Marque só o que o seu sistema trata. Sem nenhum marcado, o endpoint recebe todos.Passo 3: Guarde o secret
A resposta do cadastro traz osecret(whsec_…) uma única vez. É com ele que você valida cada entrega. Dá para rotacionar o secret a qualquer momento: o antigo deixa de valer na hora.Passo 4: Teste
O botão Testar envia umwebhook.testpelo mesmo caminho dos eventos reais (assinatura, timeout e reentrega), sem custo. O histórico de entregas mostra status, código HTTP e erro de cada uma.
Eventos
| Evento | Quando dispara | data |
|---|---|---|
query.completed | A consulta chegou ao estado final com sucesso. | { id, servico, status } |
query.failed | A consulta falhou em definitivo. O valor foi estornado. | { id, servico, status } |
vehicle.restriction.created | O monitoramento detectou restrição nova na placa, inclusive registro de roubo ou furto. | { placa, tipo, titulo, detalhe } |
vehicle.gravame.created | O monitoramento detectou gravame registrado. | { placa, tipo, titulo, detalhe } |
vehicle.gravame.removed | O monitoramento detectou baixa de gravame. | { placa, tipo, titulo, detalhe } |
vehicle.debt.created | O monitoramento detectou débito novo (detalhe traz a categoria, ex.: IPVA). | { placa, tipo, titulo, detalhe } |
vehicle.document.changed | O monitoramento detectou comunicação de venda registrada. | { placa, tipo, titulo, detalhe } |
webhook.test | Você disparou pelo portal, para conferir a integração. | { mensagem, webhook_id } |
vehicle.* só existem para placas no Monitoramento (contratado pelo portal): nascem da comparação entre uma checagem e a anterior, não de vigilância em tempo real. Mudança de situação do veículo sem restrição nova aparece no portal e por e-mail, mas não gera webhook.Os eventos query.* são da conta, não só da API
id que criou e ignore os demais em vez de tratá-los como erro.O que chega no seu endpoint
Um POST com corpo JSON e estes cabeçalhos:
| Cabeçalho | Conteúdo |
|---|---|
X-ZapCar-Event | Nome do evento (ex.: query.completed). |
X-ZapCar-Delivery | Id da entrega a este endpoint. Fica o mesmo nas reentregas da mesma entrega: útil no suporte e para cruzar com o histórico do portal. |
X-ZapCar-Signature | Assinatura no formato t=<timestamp>,v1=<hmac>. |
User-Agent | ZapCar-Webhooks/1 |
Content-Type | application/json |
{
"id": "b2c3d4e5-6f70-...", // id do EVENTO: use para idempotência
"type": "query.completed",
"created_at": "2026-08-19T12:00:00.000Z",
"data": {
"id": "a1b2c3d4-...", // id da consulta (GET /v1/consultas/:id)
"servico": "CONSULTA_COMPLETA", // código INTERNO do serviço (não é o slug da API)
"status": "CONCLUIDO" // status INTERNO gravado na consulta
}
}O evento não traz o resultado da consulta
GET /v1/consultas/{id}. É proposital: o que trafega até a sua URL é mínimo, e o dado sensível só sai por chamada autenticada com a sua chave.data.servico e data.status usam os nomes internos
data.servico é o código interno do serviço, diferente do slug que você manda no POST, e data.status é o status gravado na consulta, não o processando/concluido/erro do GET. Para decidir, use o type do evento e leia o resultado em GET /v1/consultas/{id}, que responde com o slug e o status públicos.
| data.servico | Slug na API |
|---|---|
CONSULTA | consulta |
CONSULTA_COMPLETA | consulta-completa |
CONSULTA_VEICULAR | consulta-veicular |
ECV_PREMIUM | ecv-premium |
GRAVAMES | gravame |
RENAJUD | renajud |
DEBITOS | debitos |
DEBITOS_ESTADUAL | debitos-estadual |
SC | codigo-seguranca |
CRLV | crlv |
ATPVE | atpve |
DECODIFICACAO_CHASSI | decodificacao-chassi |
DECODIFICACAO_MOTOR | decodificacao-motor |
LOCALIZACAO_SCORE | score-credito |
SITUACAO_CPF | situacao-cpf |
CONSULTA_NFE | consulta-nfe |
HISTORICO_PROPRIETARIOS | historico-proprietarios |
COMUNICACAO_VENDA | comunicacao-venda |
| Consultas de pessoa | o slug em MAIÚSCULAS, com _ no lugar de - (ex.: CNH_NACIONAL, DIVIDA_ATIVA) |
Em query.completed, data.status vem CONCLUIDO (ou PAGO na Consulta Simples). Em query.failed, vem o status de falha gravado (ex.: ERRO, ERRO_CONSULTA, ERRO_EMISSAO). Não programe contra esses valores: eles descrevem o armazenamento interno e podem ganhar variações. O contrato estável é o type.
Validar a assinatura
O cabeçalho X-ZapCar-Signature traz t (timestamp Unix, em segundos) e v1 (HMAC-SHA256 em hexadecimal). Para validar: calcule o HMAC-SHA256 de <t>.<corpo bruto> com o seu secret, compare com v1 em tempo constante e recuse timestamps com mais de 5 minutos de diferença (proteção contra replay).
const crypto = require("crypto");
// Corpo BRUTO (Buffer), não req.body já parseado: a assinatura é sobre os bytes.
app.post("/webhooks/zapcar", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-ZapCar-Signature") || "";
const t = /t=(\d+)/.exec(header)?.[1];
const v1 = /v1=([a-f0-9]+)/.exec(header)?.[1];
if (!t || !v1) return res.sendStatus(400);
// Rejeita evento antigo: barra o replay de uma entrega capturada.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400);
const esperado = crypto
.createHmac("sha256", process.env.ZAPCAR_WEBHOOK_SECRET)
.update(t + "." + req.body.toString("utf8"))
.digest("hex");
// Comparação em tempo constante.
const a = Buffer.from(esperado), b = Buffer.from(v1);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
const evento = JSON.parse(req.body.toString("utf8"));
// Responda 2xx RÁPIDO e processe depois (fila). Demorar aqui vira retry nosso.
res.sendStatus(200);
processarDepois(evento); // ignore se evento.id já foi processado
});Reentrega, ordem e duplicatas
| Regra | Comportamento |
|---|---|
| Sucesso | Qualquer resposta 2xx. Outro status, erro de rede ou tempo esgotado contam como falha. |
| Tempo para responder | 10 segundos. Responda 2xx logo e processe depois (fila). |
| Tentativas | Até 6 no total (a primeira e mais 5), com espera exponencial a partir de 10 s: 10 s, 20 s, 40 s, 80 s e 160 s. A última tentativa acontece cerca de 5 minutos depois da primeira. |
| Endpoint desativado ou removido | A entrega não é reenviada e fica como falha no histórico. |
| Destino na rede interna | Recusado antes do envio (inclusive se o DNS mudar depois do cadastro). Não é reenviado. |
| Duplicatas | Uma reentrega leva o mesmo id de evento. Se a sua URL respondeu 200 mas a resposta se perdeu, o evento chega de novo. |
| Vários endpoints | Cada endpoint recebe a sua entrega, com id de evento próprio. Não use o id para deduplicar entre endpoints diferentes. |
| Ordem | Não garantida. Um query.completed pode chegar depois de uma reentrega antiga. Use created_at e o estado atual (GET) em vez da ordem de chegada. |
Processe de forma idempotente
id de cada evento processado e ignore repetidos. Se o processamento atualiza um pedido, torne a atualização idempotente também: aplicar query.completed duas vezes no mesmo pedido não pode gerar dois lançamentos.Quando o seu endpoint fica fora do ar
Esgotadas as tentativas, a entrega fica marcada como falha no histórico do portal e não é reenviada depois. O resultado da consulta não se perde: ele continua em GET /v1/consultas/{id}. Por isso, mantenha uma reconciliação: de tempos em tempos, consulte os id que você criou e ainda não viu concluir. Webhook é o caminho rápido; a reconciliação é a rede de segurança.
Simulador de payload
Gere um evento assinado com o mesmo algoritmo do backend e dispare contra o seu ambiente de desenvolvimento, ou cole uma entrega recebida para conferir a assinatura. Tudo é calculado neste navegador; nada é enviado à ZapCar. Para o teste ponta a ponta, use o Testar do portal.