Integração com ERP automotivo
Como o sistema de uma revenda pede a consulta e mostra o resultado dentro do próprio ERP.
Como o sistema de uma revenda (ERP, DMS, sistema de despachante) pede uma consulta veicular e mostra o resultado na própria tela, sem expor a chave e sem pagar duas vezes pela mesma consulta.
Arquitetura
- Só o backend do ERP fala com a ZapCar. A tela do ERP conversa com o seu backend; a chave fica numa variável de ambiente do servidor.
- Uma linha por tentativa numa tabela sua, criada antes de chamar a API. A
Idempotency-Keysai dessa linha: se o processo cair, o reprocessamento repete a mesma chave. - Resposta assíncrona para a tela. O backend devolve “em processamento” na hora; o resultado chega pelo webhook ou por um worker de acompanhamento.
- A tela lê o bloco
veiculo, a camada estável, e tratanullcomo “não verificado”.
Passo a passo
Passo 1: Crie a tabela de consultas
SQLPostgreSQL -- Uma linha por TENTATIVA de consulta. O pedido do seu ERP (OS, ficha do -- veículo, proposta) é o pedido_ref; a Idempotency-Key deriva dele. CREATE TABLE consultas_zapcar ( id BIGSERIAL PRIMARY KEY, pedido_ref TEXT NOT NULL, tentativa INT NOT NULL DEFAULT 1, servico TEXT NOT NULL, placa TEXT, idempotency_key TEXT NOT NULL UNIQUE, zapcar_id UUID UNIQUE, status TEXT NOT NULL DEFAULT 'pendente', -- pendente | processando | concluido | erro erro_codigo TEXT, retryable BOOLEAN, resultado JSONB, -- guarde só o que a tela usa (LGPD: minimização) valor_cobrado NUMERIC(10,2), criado_em TIMESTAMPTZ NOT NULL DEFAULT now(), atualizado_em TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (pedido_ref, servico, tentativa) ); CREATE INDEX ON consultas_zapcar (status) WHERE status = 'processando';Passo 2: Exponha endpoints internos para a tela
O backend cria a consulta, devolve o andamento e serve o PDF. O exemplo usa Node.js com Express epg; a lógica é a mesma em qualquer linguagem.Node.jsbackend do ERP Passo 3: Acompanhe até o fim
Com webhook, o eventoquery.completeddispara a atualização (ver o guia de webhooks). Sem webhook, ou como rede de segurança, um worker consulta as pendentes:Node.jsworker de acompanhamento // Worker de acompanhamento (se você não usa webhook, ou como reconciliação). // Roda a cada 5 s; respeita o teto de 5 min por consulta e o limite de requisições. async function acompanhar() { const { rows } = await db.query( `SELECT * FROM consultas_zapcar WHERE status = 'processando' AND zapcar_id IS NOT NULL ORDER BY atualizado_em LIMIT 20`); for (const c of rows) { const resp = await zapcar(`/v1/consultas/${c.zapcar_id}`); if (resp.status === 429) break; // espere o próximo ciclo if (!resp.ok) continue; const r = await resp.json(); if (r.status === "processando") { await db.query("UPDATE consultas_zapcar SET atualizado_em = now() WHERE id = $1", [c.id]); continue; } await db.query( `UPDATE consultas_zapcar SET status = $2, erro_codigo = $3, retryable = $4, resultado = $5, atualizado_em = now() WHERE id = $1`, [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 ?? [], pdf_url: r.pdf_url }) : null]); } } setInterval(() => acompanhar().catch(console.error), 5000);Passo 4: Mostre o resultado na tela do ERP
Node.jsdo veiculo para a tela // Traduz o bloco "veiculo" para a tela do ERP SEM transformar falta de // informação em "nada consta". null = não verificado, sempre. function resumoParaTela({ veiculo: v, nao_verificado = [] }) { if (!v) return { disponivel: false }; const tri = (x) => (x === true ? "sim" : x === false ? "não" : "não verificado"); const debitos = v.debitos ?? []; return { disponivel: true, placa: v.placa, veiculo: [v.marca, v.modelo].filter(Boolean).join(" "), ano: `${v.ano_fabricacao ?? "?"}/${v.ano_modelo ?? "?"}`, restricoes_ativas: (v.restricoes ?? []).filter((r) => r.ativa).map((r) => r.descricao ?? r.tipo), sinistro: tri(v.sinistro), recall: tri(v.recall), leilao: v.leilao == null ? "não verificado" : (v.leilao.consta || v.leilao.fotos > 0) ? "sim" : "não", debitos: debitos.map((d) => ({ tipo: d.tipo, valor: d.valor_informado ? d.valor_centavos / 100 : null })), // Com algum débito sem valor informado, o total é um PISO, não o devido. debitos_minimo: (v.debitos_total_centavos ?? 0) / 100, debito_sem_valor: debitos.some((d) => d.valor_informado === false), nao_verificado, }; }
Nova tentativa depois de uma falha
Quando a linha termina em erro com retryable: true, crie uma linha nova com tentativa = 2 e chave erp-OS-2026-0412-consulta-t2. O valor da primeira já voltou ao saldo; a segunda é um débito novo. Com retryable false ou ausente, mostre o motivo ao usuário e deixe a decisão com ele.
Atenção:
Consultas feitas fora do ERP
Com webhook, o seu endpoint também recebe eventos de consultas feitas no portal ou no Telegram pela mesma conta. Procure o
data.id na sua tabela e ignore o que não encontrar.Dica:
Para homologar, rode o fluxo inteiro com a chave de teste e as placas
TST0002 (gravame), TST0004 (débito sem valor) e TST0005 (sinistro e leilão): são os casos que mais exigem cuidado na tela.