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.
Rode tudo no sandbox primeiro
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
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
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)
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
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>
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>
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>
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
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)
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).
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.
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.
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" });Quer que a IA escreva isso no seu projeto?