Pular para o conteúdo
ZapCar

Exemplos prontos para integrar

Código pronto em cURL, JavaScript, TypeScript, Node.js, Next.js, Python e PHP, do cliente ao webhook, com tratamento de erro.

Código para copiar e rodar, conferido contra o OpenAPI publicado. Cada linguagem começa por um cliente (o exemplo Autenticação): ele lê a chave do ambiente, aplica timeout e transforma o envelope de erro da API numa exceção com status, codigo, retryable e request_id. Os outros exemplos importam esse cliente, como numa integração de verdade.

Dica:

Rode tudo no sandbox primeiro

Com uma chave zc_test_ nada é cobrado. A placa escolhe o desfecho: TST0001 conclui limpo, TST0404 termina em erro definitivo, TST0503 em erro transitório e TST9999 nunca conclui. Ver Sandbox.

Linguagem de todos os exemplos

Autenticação

O cliente da API (chave no ambiente, timeout e erro padronizado) e uma chamada gratuita que confirma que a chave funciona.

Usa:GET /v1/servicos

Requisitos
Node.js 18 ou mais novo (fetch nativo). Sem dependências.
Variáveis
ZAPCAR_API_KEY
Como executar
node verificar-chave.mjs
JavaScriptzapcar.mjs
// zapcar.mjs: cliente da API ZapCar. SÓ NO SERVIDOR: a chave dá acesso ao saldo da conta.
const BASE = "https://api.zapcarconsulta.com.br";

export class ZapCarErro extends Error {
  constructor(status, corpo = {}, retryAfter = null) {
    super(corpo.erro ?? "HTTP " + status);
    this.status = status;                     // 0 = rede/timeout (não chegou resposta)
    this.codigo = corpo.codigo ?? null;       // ex.: PLACA_INVALIDA, SALDO_INSUFICIENTE
    this.retryable = corpo.retryable === true; // ausente = não repita sozinho
    this.requestId = corpo.request_id ?? null; // informe no suporte
    this.retryAfter = retryAfter;              // segundos, só no 429
    this.corpo = corpo;
  }
}

function chave() {
  const k = process.env.ZAPCAR_API_KEY ?? "";
  if (!/^zc_(test|live)_/.test(k)) throw new Error("Defina ZAPCAR_API_KEY com uma chave zc_test_ ou zc_live_");
  return k;
}

/** Resposta crua (para PDF e imagens). */
export async function zapcarFetch(caminho, init = {}) {
  const headers = { Authorization: "Bearer " + chave(), ...(init.body ? { "Content-Type": "application/json" } : {}), ...init.headers };
  try {
    return await fetch(BASE + caminho, { ...init, headers, signal: init.signal ?? AbortSignal.timeout(30_000) });
  } catch (e) {
    // Timeout ou rede: não dá para saber se o servidor processou.
    // No POST que cobra, é a Idempotency-Key que torna o retry seguro.
    throw new ZapCarErro(0, { erro: "Falha de rede: " + e.message, codigo: "NETWORK_ERROR", retryable: true });
  }
}

/** JSON já conferido: devolve o corpo no 2xx, lança ZapCarErro no resto. */
export async function zapcar(caminho, init = {}) {
  const resp = await zapcarFetch(caminho, init);
  const corpo = await resp.json().catch(() => ({}));
  if (!resp.ok) throw new ZapCarErro(resp.status, corpo, Number(resp.headers.get("Retry-After")) || null);
  return corpo;
}
JavaScriptverificar-chave.mjs
import { zapcar, ZapCarErro } from "./zapcar.mjs";

const ambiente = process.env.ZAPCAR_API_KEY?.startsWith("zc_test_") ? "teste (não cobra)" : "produção (cobra saldo)";

try {
  const { servicos } = await zapcar("/v1/servicos"); // gratuito
  console.log("Chave válida no ambiente de " + ambiente + ":", servicos.length, "serviços liberados.");
} catch (e) {
  if (e instanceof ZapCarErro && e.status === 401) {
    // MISSING_API_KEY ou INVALID_API_KEY: repetir não resolve. Gere outra chave no Portal.
    console.error("Chave recusada:", e.codigo, "request_id:", e.requestId);
  } else if (e instanceof ZapCarErro) {
    console.error("Falha:", e.status, e.codigo, e.message, "request_id:", e.requestId);
  } else {
    console.error(e.message); // configuração local (ex.: ZAPCAR_API_KEY ausente)
  }
  process.exitCode = 1;
}

Listar serviços e preços

O catálogo liberado para a sua conta, com o preço que será debitado e os campos que cada serviço exige. Leia daqui, nunca fixe preço no código.

Usa:GET /v1/servicos

Requisitos
Node.js 18 ou mais novo (fetch nativo). Sem dependências. Usa o zapcar.mjs do exemplo Autenticação, na mesma pasta.
Variáveis
ZAPCAR_API_KEY
Como executar
node listar-servicos.mjs
JavaScriptlistar-servicos.mjs
import { zapcar } from "./zapcar.mjs";

const reais = (v) => "R$ " + v.toFixed(2).replace(".", ",");
const { servicos } = await zapcar("/v1/servicos");

for (const s of servicos) {
  // "preco" é o que a ZapCar debita da SUA conta (já com descontos). No CRLV o preço é por UF.
  const preco = s.preco !== undefined
    ? reais(s.preco)
    : "por UF: " + (s.preco_por_uf ?? []).filter((u) => u.disponivel !== false).map((u) => u.uf + " " + reais(u.preco)).join(", ");
  const rota = s.endpoint ?? "POST /v1/consultas"; // a FIPE tem endpoint próprio e síncrono
  console.log(s.servico.padEnd(26), preco.padEnd(12), "exige:", s.requer.join(", ") || "-", "|", rota);
}

Consultar saldo

O saldo da carteira, em reais, com um alerta antes de acabar. Sem saldo, a criação de consulta devolve 402 SALDO_INSUFICIENTE.

Usa:GET /v1/saldo

Requisitos
Node.js 18 ou mais novo (fetch nativo). Sem dependências. Usa o zapcar.mjs do exemplo Autenticação, na mesma pasta.
Variáveis
ZAPCAR_API_KEYZAPCAR_SALDO_MINIMO (opcional)
Como executar
node saldo.mjs (agende com cron para monitorar)
JavaScriptsaldo.mjs
import { zapcar } from "./zapcar.mjs";

const MINIMO = Number(process.env.ZAPCAR_SALDO_MINIMO ?? 50);
const { saldo } = await zapcar("/v1/saldo"); // em reais

console.log("Saldo: R$ " + saldo.toFixed(2));
if (saldo < MINIMO) {
  // Avise o responsável ANTES de acabar: sem saldo, POST /v1/consultas responde 402.
  console.warn("Saldo abaixo de R$ " + MINIMO + ". Recarregue na Carteira do Portal do Cliente.");
  process.exitCode = 2;
}

Criar consulta

Cria a consulta com Idempotency-Key e trata cada resposta: 201 criada, 200 reaproveitada, 400, 402, 409 e 422. O resultado não vem aqui: guarde o id.

Usa:POST /v1/consultas

Requisitos
Node.js 18 ou mais novo (fetch nativo). Sem dependências. Usa o zapcar.mjs do exemplo Autenticação, na mesma pasta.
Variáveis
ZAPCAR_API_KEY
Como executar
node criar-consulta.mjs TST0001 pedido-984281
JavaScriptcriar-consulta.mjs
import { zapcar, ZapCarErro } from "./zapcar.mjs";

const placa = (process.argv[2] ?? "TST0001").toUpperCase().replace(/[^A-Z0-9]/g, "");
// O id do pedido no SEU sistema: se o processo cair e repetir, a chave volta igual e nada é cobrado de novo.
const pedido = process.argv[3] ?? "pedido-exemplo-001";

try {
  const c = await zapcar("/v1/consultas", {
    method: "POST",
    headers: { "Idempotency-Key": pedido + "-t1" },
    body: JSON.stringify({ servico: "consulta", placa }),
  });
  // 201 = criada (ou replay da mesma chave); 200 = idêntica já em processamento (reaproveitada, nada cobrado).
  console.log("id:", c.id, "| status:", c.status, "| cobrado: R$", c.valor_cobrado, "| saldo: R$", c.saldo_restante, c.ambiente === "test" ? "(sandbox)" : "");
  // Próximo passo: guardar c.id e acompanhar (polling ou webhook).
} catch (e) {
  if (!(e instanceof ZapCarErro)) throw e;
  if (e.status === 400) console.error("Pedido inválido, corrija antes de repetir:", e.codigo, e.corpo.detalhes ?? "");
  else if (e.status === 402) console.error("Saldo insuficiente. Saldo atual: R$", e.corpo.saldo);
  else if (e.status === 409) console.error("Mesma Idempotency-Key ainda em processamento: espere e repita com a MESMA chave.");
  else if (e.status === 422) console.error("Não processável:", e.codigo); // IDEMPOTENCY_KEY_REUSED, SERVICO_INDISPONIVEL...
  else console.error("Falha:", e.status, e.codigo, "retryable:", e.retryable, "request_id:", e.requestId);
  process.exitCode = 1;
}

Acompanhar processamento

Polling a cada 4 segundos com teto de 5 minutos, respeitando 429 e Retry-After. processando segue; concluido e erro são terminais.

Usa:GET /v1/consultas/{id}

Requisitos
Node.js 18 ou mais novo (fetch nativo). Sem dependências. Usa o zapcar.mjs do exemplo Autenticação, na mesma pasta.
Variáveis
ZAPCAR_API_KEY
Como executar
node acompanhar.mjs <id>
JavaScriptacompanhar.mjs
import { zapcar, ZapCarErro } from "./zapcar.mjs";

const dormir = (ms) => new Promise((r) => setTimeout(r, ms));

async function aguardarConclusao(id, { intervaloMs = 4000, tetoMs = 5 * 60_000 } = {}) {
  const limite = Date.now() + tetoMs;
  while (Date.now() < limite) {
    try {
      const c = await zapcar("/v1/consultas/" + encodeURIComponent(id));
      if (c.status !== "processando") return c; // "concluido" e "erro" não mudam mais
    } catch (e) {
      if (!(e instanceof ZapCarErro)) throw e;
      if (e.status === 429) { await dormir((e.retryAfter ?? 5) * 1000); continue; } // respeite o Retry-After
      if (e.status !== 0 && e.status < 500) throw e; // 401, 404: repetir não resolve
      // rede ou 5xx: tenta de novo no próximo ciclo
    }
    await dormir(intervaloMs);
  }
  // Timeout LOCAL: a consulta não se perdeu. Guarde o id e volte depois (ou use webhook).
  throw new Error("Ainda processando depois de " + tetoMs / 1000 + " s. id: " + id);
}

const c = await aguardarConclusao(process.argv[2]);
if (c.status === "concluido") console.log("Concluída. PDF:", c.pdf_url);
else console.log("Falhou:", c.erro_codigo, "| retryable:", c.retryable, "| valor estornado ao saldo");

Obter resultado completo

Lê a consulta concluída do jeito certo: schema_version, o bloco veiculo normalizado, o dados de cada serviço e a diferença entre null (não verificado) e "nada consta".

Usa:GET /v1/consultas/{id}

Requisitos
Node.js 18 ou mais novo (fetch nativo). Sem dependências. Usa o zapcar.mjs do exemplo Autenticação, na mesma pasta.
Variáveis
ZAPCAR_API_KEY
Como executar
node resultado.mjs <id>
JavaScriptresultado.mjs
import { zapcar } from "./zapcar.mjs";

const c = await zapcar("/v1/consultas/" + encodeURIComponent(process.argv[2]));

if (c.status === "processando") {
  console.log("Ainda processando. Volte em alguns segundos.");
} else if (c.status === "erro") {
  // Terminal: o valor já foi estornado. retryable === true: crie uma consulta NOVA, com outra Idempotency-Key.
  console.log("Falhou:", c.erro_codigo, "|", c.erro, "| retryable:", c.retryable);
} else {
  if (!String(c.schema_version).startsWith("1.")) console.warn("schema_version inesperada:", c.schema_version);
  // veiculo: bloco normalizado pela ZapCar, mesma forma em qualquer fonte (só nos serviços de veículo).
  const v = c.veiculo ?? {};
  console.log([v.marca, v.modelo, v.ano_modelo, v.uf].filter(Boolean).join(" "), "| placa", c.placa);
  // dados: o resultado do serviço, no formato de cada um (veja a página do serviço).
  console.log("Blocos em dados:", Object.keys(c.dados ?? {}).join(", ") || "(sem dados: serviço só de documento)");
  // null = NÃO verificado. "NADA CONSTA"/false = verificado e sem registro. Nunca trate os dois igual.
  if (c.nao_verificado?.length) console.log("Não verificado nesta consulta:", c.nao_verificado.join(", "));
  if (c.pdf_url) console.log("Documento:", c.pdf_url, "(baixe com a chave; veja o exemplo Baixar PDF)");
}

Baixar PDF

Baixa o documento da consulta concluída e respeita o Content-Type (alguns documentos oficiais chegam como imagem). Gratuito; no sandbox responde 422 SANDBOX_SEM_DOCUMENTO.

Usa:GET /v1/consultas/{id}/pdf

Requisitos
Node.js 18 ou mais novo (fetch nativo). Sem dependências. Usa o zapcar.mjs do exemplo Autenticação, na mesma pasta.
Variáveis
ZAPCAR_API_KEY
Como executar
node baixar-pdf.mjs <id>
JavaScriptbaixar-pdf.mjs
import { writeFile } from "node:fs/promises";
import { zapcarFetch } from "./zapcar.mjs";

const id = process.argv[2];
const resp = await zapcarFetch("/v1/consultas/" + encodeURIComponent(id) + "/pdf");

if (!resp.ok) {
  const e = await resp.json().catch(() => ({}));
  // 400 PDF_NOT_READY (retryable): ainda gerando, tente em instantes.
  // 422 SANDBOX_SEM_DOCUMENTO: o sandbox não gera documento. 404: id inexistente ou de outra conta.
  console.error("Sem documento:", resp.status, e.codigo, "retryable:", e.retryable);
  process.exit(1);
}

// Confie no Content-Type, não na extensão: alguns documentos oficiais chegam como imagem.
const tipo = resp.headers.get("content-type") ?? "";
const ext = tipo.includes("pdf") ? "pdf" : tipo.includes("png") ? "png" : tipo.includes("jpeg") ? "jpg" : "bin";
await writeFile("consulta-" + id + "." + ext, Buffer.from(await resp.arrayBuffer()));
console.log("Salvo:", "consulta-" + id + "." + ext, "(" + tipo + ")");

Consultar Tabela FIPE

FIPE pela placa na mesma chamada (síncrono, sem polling). Cobra por requisição e estorna se não houver FIPE: use Idempotency-Key.

Usa:POST /v1/fipe

Requisitos
Node.js 18 ou mais novo (fetch nativo). Sem dependências. Usa o zapcar.mjs do exemplo Autenticação, na mesma pasta.
Variáveis
ZAPCAR_API_KEY
Como executar
node fipe.mjs ABC1D23 pedido-984281
JavaScriptfipe.mjs
import { zapcar, ZapCarErro } from "./zapcar.mjs";

const placa = (process.argv[2] ?? "ABC1D23").toUpperCase().replace(/[^A-Z0-9]/g, "");
const pedido = process.argv[3] ?? "pedido-exemplo-001";

try {
  // Síncrono: o resultado vem nesta resposta. Débito antes da chamada à fonte: a chave evita cobrança dupla.
  const r = await zapcar("/v1/fipe", {
    method: "POST",
    headers: { "Idempotency-Key": pedido + "-fipe" },
    body: JSON.stringify({ placa }),
  });
  for (const f of r.fipe) console.log(f.codigo_fipe, "|", f.modelo, f.ano_modelo, "|", f.valor, "(" + f.mes_referencia + ")");
  console.log("Cobrado: R$", r.valor_cobrado, "| saldo: R$", r.saldo_restante);
} catch (e) {
  if (!(e instanceof ZapCarErro)) throw e;
  if (e.status === 404) console.error("Sem FIPE para esta placa. O valor foi estornado."); // FIPE_NAO_DISPONIVEL
  else if (e.status === 402) console.error("Saldo insuficiente:", e.corpo.saldo);
  else console.error("Falha:", e.status, e.codigo, "retryable:", e.retryable); // 502/503: transitório, já estornado
  process.exitCode = 1;
}

Receber webhooks

Endpoint que valida a assinatura sobre o corpo bruto, responde 2xx em menos de 10 s, ignora evento repetido pelo id e só então busca o resultado.

Usa:GET /v1/consultas/{id}

Requisitos
Node.js 18 ou mais novo (fetch nativo). Sem dependências. Usa o assinatura.mjs do exemplo Validar assinatura. Usa o zapcar.mjs do exemplo Autenticação, na mesma pasta.
Variáveis
ZAPCAR_API_KEYZAPCAR_WEBHOOK_SECRET
Como executar
node webhook.mjs (servidor HTTP nativo, sem framework)
JavaScriptwebhook.mjs
import { createServer } from "node:http";
import { verificarAssinatura } from "./assinatura.mjs";
import { zapcar } from "./zapcar.mjs";

const vistos = new Set(); // em produção: tabela com o id do evento como chave única

createServer((req, res) => {
  if (req.method !== "POST" || req.url !== "/webhooks/zapcar") { res.writeHead(404).end(); return; }
  const partes = [];
  req.on("data", (p) => partes.push(p));
  req.on("end", () => {
    const bruto = Buffer.concat(partes); // a assinatura é sobre estes bytes, antes de qualquer parse
    if (!verificarAssinatura(bruto, req.headers["x-zapcar-signature"], process.env.ZAPCAR_WEBHOOK_SECRET)) {
      res.writeHead(401).end();
      return;
    }
    const evento = JSON.parse(bruto.toString("utf8"));
    res.writeHead(200).end(); // responda já: mais de 10 s vira reentrega
    if (vistos.has(evento.id)) return; // reentrega traz o MESMO id
    vistos.add(evento.id);
    processar(evento).catch((e) => console.error("webhook", evento.id, e.message));
  });
}).listen(3000);

async function processar(evento) {
  if (!evento.type.startsWith("query.")) return; // webhook.test e eventos de monitoramento
  // O evento não traz o resultado: busque-o pelo id da consulta.
  const c = await zapcar("/v1/consultas/" + encodeURIComponent(evento.data.id));
  console.log(evento.type, c.id, c.status); // atualize o pedido no seu banco aqui
}

Validar assinatura

HMAC-SHA256 de "<t>.<corpo bruto>" com o secret do endpoint, comparado em tempo constante, recusando timestamp com mais de 5 minutos (replay).

Requisitos
Node.js 18 ou mais novo (fetch nativo). Sem dependências.
Variáveis
ZAPCAR_WEBHOOK_SECRET
Como executar
Importe verificarAssinatura no seu endpoint (veja Receber webhooks).
JavaScriptassinatura.mjs
import crypto from "node:crypto";

/**
 * X-ZapCar-Signature: t=<unix>,v1=<hex>. O HMAC-SHA256 é de "<t>.<corpo bruto>".
 * corpoBruto: Buffer ou string EXATAMENTE como chegou (parse + stringify invalida a assinatura).
 */
export function verificarAssinatura(corpoBruto, cabecalho, secret, toleranciaS = 300) {
  if (!cabecalho || !secret) return false;
  const partes = String(cabecalho).split(",").map((p) => p.trim().split("="));
  const t = partes.find(([k]) => k === "t")?.[1];
  const assinaturas = partes.filter(([k]) => k === "v1").map(([, v]) => v);
  if (!t || !/^\d+$/.test(t) || assinaturas.length === 0) return false;

  // Replay: recusa entrega capturada e reenviada depois.
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranciaS) return false;

  const esperado = crypto.createHmac("sha256", secret).update(t + ".").update(corpoBruto).digest();
  return assinaturas.some((v1) => {
    const recebido = Buffer.from(v1, "hex");
    return recebido.length === esperado.length && crypto.timingSafeEqual(recebido, esperado); // tempo constante
  });
}

Tratar erros

Os dois tipos de falha (erro da requisição e consulta com status erro), o campo retryable, o 429 com Retry-After e backoff com jitter só onde repetir é seguro.

Usa:GET /v1/consultas/{id}

Requisitos
Node.js 18 ou mais novo (fetch nativo). Sem dependências. Usa o zapcar.mjs do exemplo Autenticação, na mesma pasta.
Variáveis
ZAPCAR_API_KEY
Como executar
Importe comRetry e acaoParaErro onde chama a API.
JavaScripterros.mjs
import { ZapCarErro } from "./zapcar.mjs";

const dormir = (ms) => new Promise((r) => setTimeout(r, ms));

/** O que fazer com um erro de REQUISIÇÃO (envelope ApiError). */
export function acaoParaErro(e) {
  if (!(e instanceof ZapCarErro)) return "bug";            // erro do seu código
  if (e.status === 401) return "trocar_chave";            // não repita
  if (e.status === 402) return "recarregar_saldo";        // nada foi cobrado
  if (e.status === 429) return "esperar_retry_after";
  if (e.retryable) return "repetir_com_backoff";          // 5xx, rede, 409 em andamento, PDF_NOT_READY
  return "corrigir_pedido";                               // 400/404/422: repetir igual dá o mesmo erro
}

/**
 * Repete só o que é seguro: GET, ou POST com Idempotency-Key ESTÁVEL entre as tentativas.
 * Backoff exponencial com jitter; no 429 vale o Retry-After.
 */
export async function comRetry(fn, { tentativas = 4, baseMs = 1000 } = {}) {
  for (let i = 1; ; i++) {
    try {
      return await fn();
    } catch (e) {
      const acao = acaoParaErro(e);
      if (i >= tentativas || (acao !== "repetir_com_backoff" && acao !== "esperar_retry_after")) throw e;
      const espera = acao === "esperar_retry_after"
        ? (e.retryAfter ?? 5) * 1000
        : baseMs * 2 ** (i - 1) + Math.random() * baseMs; // 1 s, 2 s, 4 s... + jitter
      console.warn("zapcar", e.status, e.codigo, "request_id", e.requestId, "nova tentativa em", Math.round(espera), "ms");
      await dormir(espera);
    }
  }
}

// Consulta com status "erro" NÃO é exceção: é um resultado terminal.
// c.retryable === true: crie uma consulta NOVA (Idempotency-Key nova). false ou ausente: mostre c.erro ao usuário.

Evitar cobranças duplicadas

Idempotency-Key derivada do SEU pedido: o retry de rede repete a mesma chave e não cobra de novo; só uma consulta nova (depois de um erro retryable) ganha chave nova.

Usa:POST /v1/consultas

Requisitos
Node.js 18 ou mais novo (fetch nativo). Sem dependências. Usa também o acompanhar.mjs (adapte para exportar aguardarConclusao). Usa o zapcar.mjs do exemplo Autenticação, na mesma pasta.
Variáveis
ZAPCAR_API_KEY
Como executar
Importe consultarSemDuplicar no seu job de pedidos.
JavaScriptsem-duplicar.mjs
import { zapcar, ZapCarErro } from "./zapcar.mjs";

const dormir = (ms) => new Promise((r) => setTimeout(r, ms));

/**
 * Cria a consulta de UM pedido sem risco de cobrar duas vezes.
 * - Mesma tentativa = mesma Idempotency-Key, mesmo corpo: rede caiu, 5xx, 429 ou 409 → repete IGUAL.
 * - Tentativa nova (só depois de status "erro" com retryable true) = chave nova.
 */
export async function criarSemDuplicar(pedido, tentativa = 1) {
  const chave = "pedido-" + pedido.id + "-t" + tentativa; // grave junto do pedido
  const corpo = JSON.stringify({ servico: pedido.servico, placa: pedido.placa }); // sempre o MESMO corpo
  for (let i = 1; i <= 5; i++) {
    try {
      return await zapcar("/v1/consultas", { method: "POST", headers: { "Idempotency-Key": chave }, body: corpo });
    } catch (e) {
      if (!(e instanceof ZapCarErro)) throw e;
      if (e.codigo === "IDEMPOTENCY_KEY_REUSED") throw new Error("Bug: a chave " + chave + " já foi usada com outro corpo.");
      const repetir = e.status === 0 || e.status === 429 || e.status === 409 || e.status >= 500;
      if (!repetir || i === 5) throw e; // 400/402/422: repetir não muda nada
      await dormir(e.status === 429 ? (e.retryAfter ?? 5) * 1000 : 1000 * 2 ** (i - 1));
    }
  }
}

// Uso: const c = await criarSemDuplicar({ id: 984281, servico: "consulta", placa: "ABC1D23" });
Informação:

Quer que a IA escreva isso no seu projeto?

Os prompts da Integração com IA pedem exatamente estes padrões (cliente único, Idempotency-Key, polling com teto, assinatura do webhook), adaptados à estrutura do seu código.