Pular para o conteúdo
ZapCar
Developersv1

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

  1. 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.
  2. Passo 2: Escolha os eventos

    Marque só o que o seu sistema trata. Sem nenhum marcado, o endpoint recebe todos.
  3. Passo 3: Guarde o secret

    A resposta do cadastro traz o secret (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.
  4. Passo 4: Teste

    O botão Testar envia um webhook.test pelo 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

EventoQuando disparadata
query.completedA consulta chegou ao estado final com sucesso.{ id, servico, status }
query.failedA consulta falhou em definitivo. O valor foi estornado.{ id, servico, status }
vehicle.restriction.createdO monitoramento detectou restrição nova na placa, inclusive registro de roubo ou furto.{ placa, tipo, titulo, detalhe }
vehicle.gravame.createdO monitoramento detectou gravame registrado.{ placa, tipo, titulo, detalhe }
vehicle.gravame.removedO monitoramento detectou baixa de gravame.{ placa, tipo, titulo, detalhe }
vehicle.debt.createdO monitoramento detectou débito novo (detalhe traz a categoria, ex.: IPVA).{ placa, tipo, titulo, detalhe }
vehicle.document.changedO monitoramento detectou comunicação de venda registrada.{ placa, tipo, titulo, detalhe }
webhook.testVocê disparou pelo portal, para conferir a integração.{ mensagem, webhook_id }
Informação:
Os eventos 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.
Atenção:

Os eventos query.* são da conta, não só da API

Uma consulta feita no portal pela sua equipe, ou pelo bot do Telegram, chega ao seu webhook do mesmo jeito: o disparo olha a conta dona da consulta, não o canal. Se o seu sistema só espera o que ele mesmo pediu, guarde os 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çalhoConteúdo
X-ZapCar-EventNome do evento (ex.: query.completed).
X-ZapCar-DeliveryId 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-SignatureAssinatura no formato t=<timestamp>,v1=<hmac>.
User-AgentZapCar-Webhooks/1
Content-Typeapplication/json
JSONcorpo do evento
{
  "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
  }
}
Atenção:

O evento não traz o resultado da consulta

Ele avisa que ficou pronto; os dados saem por 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.servicoSlug na API
CONSULTAconsulta
CONSULTA_COMPLETAconsulta-completa
CONSULTA_VEICULARconsulta-veicular
ECV_PREMIUMecv-premium
GRAVAMESgravame
RENAJUDrenajud
DEBITOSdebitos
DEBITOS_ESTADUALdebitos-estadual
SCcodigo-seguranca
CRLVcrlv
ATPVEatpve
DECODIFICACAO_CHASSIdecodificacao-chassi
DECODIFICACAO_MOTORdecodificacao-motor
LOCALIZACAO_SCOREscore-credito
SITUACAO_CPFsituacao-cpf
CONSULTA_NFEconsulta-nfe
HISTORICO_PROPRIETARIOShistorico-proprietarios
COMUNICACAO_VENDAcomunicacao-venda
Consultas de pessoao 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).

Importante:
Use o corpo exatamente como chegou. Fazer parse e re-serializar o JSON antes de assinar muda os bytes e a assinatura não bate.
Node.jsverificação da assinatura
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

RegraComportamento
SucessoQualquer resposta 2xx. Outro status, erro de rede ou tempo esgotado contam como falha.
Tempo para responder10 segundos. Responda 2xx logo e processe depois (fila).
TentativasAté 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 removidoA entrega não é reenviada e fica como falha no histórico.
Destino na rede internaRecusado antes do envio (inclusive se o DNS mudar depois do cadastro). Não é reenviado.
DuplicatasUma 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 endpointsCada endpoint recebe a sua entrega, com id de evento próprio. Não use o id para deduplicar entre endpoints diferentes.
OrdemNã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.
Atenção:

Processe de forma idempotente

Guarde o 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.

Informação:
Cadastre a URL final do seu endpoint. Redirecionamentos são seguidos, mas cada salto consome parte dos 10 segundos da entrega.

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.

Dica:
Guia completo, do recebimento à atualização do pedido: Receber resultados por webhook.