Pular para o conteúdo
ZapCar
Developersv1

Receber resultados por webhook

Receber o evento, validar a assinatura, deduplicar e atualizar o pedido sem polling.

Receber o aviso de que a consulta terminou, confirmar que ele veio da ZapCar, não processar o mesmo evento duas vezes e atualizar o pedido no seu sistema. O guia continua o guia do ERP (mesma tabela consultas_zapcar), mas vale para qualquer sistema.

O que o receptor precisa fazer

  • Validar a assinatura HMAC sobre o corpo bruto e recusar timestamp com mais de 5 minutos.
  • Deduplicar pelo id do evento: reentregas trazem o mesmo id.
  • Responder 2xx em menos de 10 segundos e processar depois.
  • Buscar o resultado com GET /v1/consultas/{id}: o evento não traz o dado.
  • Atualizar o pedido de forma idempotente e sem depender da ordem de chegada.
  • Manter uma reconciliação: se a entrega esgotar as tentativas, o resultado continua disponível no GET.

Passo a passo

  1. Passo 1: Cadastre o endpoint e guarde o secret

    Em Conta › Integrações, aba Webhooks: URL HTTPS pública, eventos query.completed e query.failed. Guarde o whsec_… em ZAPCAR_WEBHOOK_SECRET.
  2. Passo 2: Crie a tabela de eventos processados

    SQLPostgreSQL
    -- Eventos já processados: a deduplicação é pelo id do EVENTO.
    CREATE TABLE zapcar_eventos (
      event_id     UUID PRIMARY KEY,
      tipo         TEXT NOT NULL,
      recebido_em  TIMESTAMPTZ NOT NULL DEFAULT now()
    );
  3. Passo 3: Implemente o receptor

    Node.jsreceptor de webhooks
    // Receptor de webhooks (Node.js 18+, Express, pg).
    import crypto from "node:crypto";
    import express from "express";
    import pg from "pg";
    
    const app  = express();
    const db   = new pg.Pool({ connectionString: process.env.DATABASE_URL });
    const BASE = "https://api.zapcarconsulta.com.br";
    const SECRET = process.env.ZAPCAR_WEBHOOK_SECRET;   // whsec_…
    const TOLERANCIA_S = 300;
    
    function assinaturaValida(cabecalho, corpoBruto) {
      const t  = /t=(\d+)/.exec(cabecalho ?? "")?.[1];
      const v1 = /v1=([a-f0-9]+)/.exec(cabecalho ?? "")?.[1];
      if (!t || !v1) return false;
      if (Math.abs(Date.now() / 1000 - Number(t)) > TOLERANCIA_S) return false;   // replay
      const esperado = crypto.createHmac("sha256", SECRET).update(`${t}.`).update(corpoBruto).digest("hex");
      const a = Buffer.from(esperado), b = Buffer.from(v1);
      return a.length === b.length && crypto.timingSafeEqual(a, b);
    }
    
    // express.raw: a assinatura é sobre os BYTES; req.body chega como Buffer.
    app.post("/webhooks/zapcar", express.raw({ type: "application/json", limit: "64kb" }), async (req, res) => {
      if (!assinaturaValida(req.get("X-ZapCar-Signature"), req.body)) return res.sendStatus(401);
      const evento = JSON.parse(req.body.toString("utf8"));
    
      // Deduplicação: um retry reenvia o MESMO id de evento.
      const { rowCount } = await db.query(
        "INSERT INTO zapcar_eventos (event_id, tipo) VALUES ($1, $2) ON CONFLICT DO NOTHING",
        [evento.id, evento.type]);
      res.sendStatus(200);                      // responda em menos de 10 s
      if (rowCount === 0) return;               // já processado
    
      // Processa DEPOIS de responder (em produção, prefira uma fila).
      processar(evento).catch((e) => console.error("webhook", evento.id, e));
    });
    
    async function processar(evento) {
      if (evento.type !== "query.completed" && evento.type !== "query.failed") return;
      const zapcarId = evento.data.id;
    
      // Só interessa o que o SEU sistema pediu (o webhook também recebe consultas
      // feitas no portal ou no Telegram pela mesma conta).
      const { rows: [c] } = await db.query("SELECT id FROM consultas_zapcar WHERE zapcar_id = $1", [zapcarId]);
      if (!c) return;
    
      // O evento não traz o resultado: busque com a sua chave.
      const resp = await fetch(`${BASE}/v1/consultas/${zapcarId}`, {
        headers: { Authorization: `Bearer ${process.env.ZAPCAR_API_KEY}` },
        signal: AbortSignal.timeout(30_000),
      });
      if (!resp.ok) throw new Error(`GET ${resp.status}`);   // a reconciliação pega depois
      const r = await resp.json();
      if (r.status === "processando") return;                  // fora de ordem: o GET manda
    
      // Atualização idempotente: aplicar duas vezes dá o mesmo resultado.
      await db.query(
        `UPDATE consultas_zapcar SET status = $2, erro_codigo = $3, retryable = $4,
           resultado = $5, atualizado_em = now()
         WHERE id = $1 AND status = 'processando'`,
        [c.id, r.status, r.erro_codigo ?? null, r.retryable ?? null,
         r.status === "concluido" ? JSON.stringify({ veiculo: r.veiculo ?? null, nao_verificado: r.nao_verificado ?? [] }) : null]);
    }
    
    app.listen(3001);
  4. Passo 4: Teste antes de ir ao ar

    Gere um evento assinado no simulador e dispare contra o seu ambiente local com o cURL gerado. Depois, use o botão Testar do portal (webhook.test) contra a URL pública. Consultas do sandbox não disparam query.*.
Atenção:

Não confie só no webhook

Esgotadas as 6 tentativas (cerca de 5 minutos), a entrega não é refeita. Mantenha o worker de acompanhamento do guia do ERP rodando com intervalo maior (por exemplo, a cada minuto) para as consultas que seguem em processando há mais tempo do que o esperado.
Dica:
Detalhes de cabeçalhos, política de reentrega e ordem de entrega estão na referência de webhooks.