Para desenvolvedores · API 2026-09-18
Conecte seu sistema ao CardZap
Receba pedidos no seu PDV, ERP ou aplicativo. Comece com uma consulta à API e adicione webhooks para acompanhar novos pedidos e mudanças de status.
Guia público: compartilhe este endereço com quem vai desenvolver a integração. Nenhuma conta é necessária para ler.
1. Seu primeiro pedido pela API
A integração permite ler pedidos e receber eventos. A API v1 não cria pedidos, altera status nem sincroniza o cardápio. Essas ações continuam no CardZap.
- No painel da loja, abra Integrações → Chaves de API.
- Crie uma chave com o nome do seu sistema e copie o valor completo. Ele aparece apenas uma vez.
- No terminal do seu servidor, defina as variáveis abaixo e execute a consulta.
export CARDZAP_API_URL='https://cardzap.io/api/v1'
export CARDZAP_API_KEY='czk_SUBSTITUA_PELA_SUA_CHAVE'
curl --fail-with-body --get "$CARDZAP_API_URL/orders" \
--header "Authorization: Bearer $CARDZAP_API_KEY" \
--data-urlencode 'limit=1'O resultado esperado é HTTP 200. Se a loja ainda não tem pedidos, data será uma lista vazia — isso também confirma que a autenticação funcionou. A listagem só inclui alterações com pelo menos 5 segundos.
| Campo / valor | Como usar |
|---|---|
| API | Seu sistema busca pedidos periodicamente. É suficiente para começar e não exige uma URL para receber eventos. |
| Webhook + API | Receba mudanças automaticamente e use a API para carga inicial e reconciliação. Recomendado para manter os pedidos sincronizados. |
2. Autenticação e acesso
URL base: https://cardzap.io/api/v1. Envie Authorization: Bearer czk_… em toda consulta. Cada chave acessa apenas os pedidos da loja onde foi criada; não é necessário enviar restaurant_id.
A chave de API (czk_…) autentica consultas. O segredo do webhook (whsec_…) valida eventos recebidos. São credenciais diferentes; use o valor completo, incluindo o prefixo.
Guarde ambos no servidor, em variáveis de ambiente. Eles não devem aparecer no código do navegador, em repositórios ou em logs. Para substituir uma chave, crie outra, atualize seu sistema e revogue a antiga no painel.
3. Consultar pedidos
GET /api/v1/orders
Retorna o estado atual dos pedidos, ordenados por updated_at crescente e, em caso de empate, por id. Todos os parâmetros são opcionais. Sem filtros, começa pelos pedidos mais antigos alterados.
| Campo / valor | Como usar |
|---|---|
| updated_since | Data ISO 8601 com fuso, por exemplo 2026-09-21T00:00:00Z. Retorna alterações estritamente posteriores à data. |
| cursor | Valor de next_cursor da chamada anterior. Trate como texto opaco: não monte nem decodifique. Tem precedência sobre updated_since. |
| limit | Inteiro de 1 a 100; padrão 50. Mantenha o limite nas próximas páginas. |
| status | Opcional: NEW, COMPLETED, CANCELLED, EXPIRED, CONFIRMED, IN_PREPARATION, READY ou OUT_FOR_DELIVERY. Mantenha o filtro nas próximas páginas. |
curl --fail-with-body --get "$CARDZAP_API_URL/orders" \
-H "Authorization: Bearer $CARDZAP_API_KEY" \
--data-urlencode 'updated_since=2026-09-21T00:00:00Z' \
--data-urlencode 'limit=50'
# Copie next_cursor da resposta anterior.
curl --fail-with-body --get "$CARDZAP_API_URL/orders" \
-H "Authorization: Bearer $CARDZAP_API_KEY" \
--data-urlencode 'cursor=COLE_NEXT_CURSOR_AQUI' \
--data-urlencode 'limit=50'{
"api_version": "2026-09-18",
"data": [
{
"id": "pedido_exemplo_42",
"restaurant_id": "loja_exemplo",
"number": 42,
"code": "0042",
"status": "NEW",
"created_at": "2026-09-21T15:00:00.000Z",
"updated_at": "2026-09-21T15:00:00.000Z",
"completed_at": null,
"cancelled_at": null,
"expired_at": null,
"fulfillment": "DELIVERY",
"fulfillment_label": "Entrega",
"table_label": null,
"customer": {
"name": "Ana (exemplo)",
"phone": "5511999999999"
},
"address": "Rua Exemplo, 100",
"notes": null,
"payment": {
"method": "PIX_ON_DELIVERY",
"label": "Pix"
},
"items": [
{
"kind": "item",
"item_id": "produto_exemplo",
"combo_id": null,
"name": "X-Burger",
"external_code": "101",
"quantity": 2,
"unit_price_cents": 2500,
"regular_unit_price_cents": null,
"options": [
{
"name": "Bacon",
"external_code": "B1",
"price_cents": 200
}
],
"components": [],
"total_cents": 5400
}
],
"promotions": [],
"promo_discount_cents": 0,
"subtotal_cents": 5400,
"delivery_fee_cents": 500,
"coupon": null,
"total_cents": 5900
}
],
"has_more": false,
"next_cursor": "MjAyNi0wOS0yMVQxNTowMDowMC4wMDBafHBlZGlkb19leGVtcGxvXzQy"
}Enquanto has_more for true, continue com next_cursor. Quando for false, salve o cursor e consulte novamente depois; o cursor também vem em respostas vazias. Só avance o cursor depois de persistir todos os pedidos da página.
GET /api/v1/orders/{orderId}
curl --fail-with-body \
-H "Authorization: Bearer $CARDZAP_API_KEY" \
"$CARDZAP_API_URL/orders/ID_DO_PEDIDO"Use o campo id, não o número de exibição code. A resposta é { "api_version": "2026-09-18", "data": { …pedido } }, sem paginação. Um pedido inexistente ou de outra loja retorna 404.
O limite atual é de 900 requisições por 15 minutos por chave (cerca de 1/s), compartilhado entre os dois endpoints e aplicado por instância do servidor. Distribua as chamadas e respeite Retry-After ao receber 429.
4. Receber pedidos automaticamente
- Disponibilize um endpoint POST em uma URL HTTPS pública, por exemplo
https://seu-sistema.com/webhooks/cardzap. - Em Integrações → Webhook, salve a URL e mantenha o webhook ativo.
- Copie o segredo de assinatura para
CARDZAP_WEBHOOK_SECRETno servidor receptor. - Use o botão de teste no painel. Seu endpoint receberá um
ping; responda com HTTP 2xx.
Localhost, IPs privados e redirecionamentos não são aceitos. Para desenvolver localmente, exponha seu receptor por um túnel HTTPS público. O endpoint precisa responder em até 10 segundos.
| Campo / valor | Como usar |
|---|---|
| Content-Type | application/json |
| CardZap-Event | Tipo do evento, também disponível em type no corpo. |
| CardZap-Delivery | Identificador da entrega para diagnóstico. Para evitar duplicações, use id do corpo. |
| CardZap-Signature | t=<segundos Unix>,v1=<HMAC-SHA256 em hexadecimal>. A assinatura cobre t + ponto + corpo bruto e é renovada em cada tentativa. |
| Campo / valor | Como usar |
|---|---|
| order.created | Pedido criado, normalmente com status NEW. |
| order.completed | Pedido concluído (COMPLETED). |
| order.cancelled | Pedido cancelado (CANCELLED). |
| order.expired | Pedido expirado (EXPIRED). |
| order.updated | Mudança em etapas de atendimento de pedidos legados. |
| ping | Teste de conexão. data contém restaurant_id; não contém order. Não há retentativa automática do ping. |
{
"id": "evt_exemplo_42",
"type": "order.created",
"api_version": "2026-09-18",
"created_at": "2026-09-21T15:00:00.000Z",
"data": {
"order": {
"id": "pedido_exemplo_42",
"restaurant_id": "loja_exemplo",
"number": 42,
"code": "0042",
"status": "NEW",
"created_at": "2026-09-21T15:00:00.000Z",
"updated_at": "2026-09-21T15:00:00.000Z",
"completed_at": null,
"cancelled_at": null,
"expired_at": null,
"fulfillment": "DELIVERY",
"fulfillment_label": "Entrega",
"table_label": null,
"customer": {
"name": "Ana (exemplo)",
"phone": "5511999999999"
},
"address": "Rua Exemplo, 100",
"notes": null,
"payment": {
"method": "PIX_ON_DELIVERY",
"label": "Pix"
},
"items": [
{
"kind": "item",
"item_id": "produto_exemplo",
"combo_id": null,
"name": "X-Burger",
"external_code": "101",
"quantity": 2,
"unit_price_cents": 2500,
"regular_unit_price_cents": null,
"options": [
{
"name": "Bacon",
"external_code": "B1",
"price_cents": 200
}
],
"components": [],
"total_cents": 5400
}
],
"promotions": [],
"promo_discount_cents": 0,
"subtotal_cents": 5400,
"delivery_fee_cents": 500,
"coupon": null,
"total_cents": 5900
}
}
}data.order tem exatamente o mesmo formato do pedido na API. O envelope representa o estado no momento do evento; created_at do envelope é a data do evento, e data.order.updated_at é a data da alteração do pedido.
Exemplo completo de receptor em Node.js
Salve como receptor.mjs. Este exemplo verifica a assinatura e confirma o recebimento, mas não importa pedidos. Antes de usar em produção, implemente uma fila ou banco persistente com unicidade por evento.id antes de responder 2xx. Se a gravação falhar, responda 5xx para permitir nova tentativa.
// receptor.mjs — Node.js 20+, sem instalar dependências
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
function assinaturaValida(rawBody, header, segredo) {
if (typeof header !== "string") return false;
const partes = Object.fromEntries(header.split(",").map((parte) => {
const i = parte.indexOf("=");
return [parte.slice(0, i).trim(), parte.slice(i + 1).trim()];
}));
const t = Number(partes.t);
if (!Number.isInteger(t) || !/^[a-f0-9]{64}$/i.test(partes.v1 ?? "")) return false;
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const esperado = createHmac("sha256", segredo)
.update(`${t}.`).update(rawBody).digest();
const recebido = Buffer.from(partes.v1, "hex");
return recebido.length === esperado.length && timingSafeEqual(recebido, esperado);
}
const segredo = process.env.CARDZAP_WEBHOOK_SECRET;
if (!segredo) throw new Error("Defina CARDZAP_WEBHOOK_SECRET");
createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/webhooks/cardzap") {
res.writeHead(404).end();
return;
}
try {
const partes = [];
let tamanho = 0;
for await (const parte of req) {
tamanho += parte.length;
if (tamanho > 1024 * 1024) { res.writeHead(413).end(); return; }
partes.push(parte);
}
const rawBody = Buffer.concat(partes);
if (!assinaturaValida(rawBody, req.headers["cardzap-signature"], segredo)) {
res.writeHead(401).end();
return;
}
const evento = JSON.parse(rawBody.toString("utf8"));
// Demonstração de recebimento: não grava nem importa pedidos.
// Em produção, persista o evento em uma fila/banco antes do 2xx.
// Use evento.id como chave única e processe fora desta requisição.
// O ping tem data.restaurant_id, sem data.order.
console.log({ id: evento.id, tipo: evento.type });
res.writeHead(204).end();
} catch {
res.writeHead(500).end();
}
}).listen(3001, () => console.log("Receptor na porta 3001"));export CARDZAP_WEBHOOK_SECRET='whsec_SUBSTITUA_PELO_SEGREDO'
node receptor.mjsValide a assinatura antes de interpretar o JSON, usando os bytes originais. Fazer JSON.parse e depois JSON.stringify pode alterar a assinatura. O exemplo recusa timestamps com mais de 5 minutos de diferença; mantenha o relógio do servidor sincronizado.
Sem resposta 2xx, são até 7 tentativas, com intervalos de 1 min, 5 min, 30 min, 2 h, 6 h e 12 h (aproximadamente 21 h no total). A fila é verificada a cada 5 minutos, portanto os intervalos não são horários exatos.
Ao pausar, eventos novos e pendentes ficam na fila sem consumir tentativas. Ao reativar, o envio é retomado pela rotina de 5 minutos. No histórico do painel, filtre falhas e reenvie eventos individualmente ou até 20 por vez, preservando o id do evento. Entregas finalizadas são removidas após 30 dias desde a criação; pendentes são preservadas até o processamento ou a remoção do webhook.
Gerar um novo segredo muda a assinatura das próximas tentativas, inclusive as pendentes. Atualize o receptor junto com essa troca.
5. Entenda os dados do pedido
Datas são strings ISO 8601 em UTC. Valores monetários são inteiros em centavos: 5900 significa R$ 59,00. Campos sem valor podem ser null; aceite também campos novos para manter compatibilidade.
| Campo / valor | Como usar |
|---|---|
| id / restaurant_id | Identificadores do pedido e da loja. Use id como chave de atualização no seu sistema. |
| number / code | Número sequencial por loja e código para exibição (ex.: 42 / 0042). Pedidos antigos podem ter number null; code tem um fallback. |
| status | Fluxo atual: NEW → COMPLETED ou CANCELLED; pedidos pendentes também podem virar EXPIRED. Etapas CONFIRMED, IN_PREPARATION, READY e OUT_FOR_DELIVERY são legadas. |
| created_at / updated_at | Criação e última alteração. Use updated_at para impedir que um estado antigo substitua um mais recente. |
| completed_at / cancelled_at / expired_at | Data da respectiva transição, ou null. |
| fulfillment / fulfillment_label | DELIVERY (entrega), PICKUP (retirada) ou DINE_IN (consumo na mesa); fulfillment_label é o texto de exibição. |
| table_label | Identificação da mesa registrada no pedido, ou null. Pedidos de mesa não expiram automaticamente. |
| customer | name (texto) e phone (texto ou null). DINE_IN não coleta telefone; não exija telefone para importar. |
| address / notes | Endereço e observações, ou null. Endereço pode estar ausente em retirada ou mesa. |
| payment | method: PIX_ON_DELIVERY, CASH, CARD_ON_DELIVERY ou null; label é o texto de exibição. Mesa não coleta pagamento. O campo indica a forma escolhida, não comprova pagamento. |
| items | Linhas do carrinho, incluindo itens e combos. Veja os campos abaixo. |
| promotions / promo_discount_cents | Descontos por quantidade aplicados. Cada promoção tem item_id, name, label e discount_cents; promo_discount_cents é o desconto total dessas promoções. |
| subtotal_cents / delivery_fee_cents | Subtotal das linhas antes dos descontos por quantidade e cupom; taxa de entrega. |
| coupon | Objeto com code e discount_cents, ou null. |
| total_cents | Valor final calculado pelo CardZap. Use este valor ao importar o pedido. |
| Campo / valor | Como usar |
|---|---|
| kind / item_id / combo_id | kind é item ou combo. Em combos, item_id é null e combo_id identifica o combo; em itens, combo_id é null. |
| name / external_code | Nome e código do produto no PDV, registrados no pedido. external_code pode ser null: combine o mapeamento com o lojista antes da integração. |
| quantity / unit_price_cents | Quantidade da linha e preço unitário cobrado, sem adicionais. Um preço promocional já está refletido aqui. |
| regular_unit_price_cents | Preço unitário original quando registrado para a promoção, ou null. Não é um desconto extra a subtrair. |
| options | Adicionais: name, external_code (ou null) e price_cents por unidade da linha. Multiplique pelo quantity da linha; não há quantity em cada adicional. |
| components | Conteúdo de uma unidade do combo: item_id, name, external_code (ou null) e quantity. Multiplique essa quantidade pelo quantity da linha. Lista vazia em itens comuns. |
| total_cents | (unit_price_cents + soma de options.price_cents) × quantity, antes de descontos por quantidade e cupom. Componentes do combo não são cobrados novamente. |
total_cents = subtotal_cents
- promo_discount_cents
+ delivery_fee_cents
- (coupon?.discount_cents ?? 0)
# Exemplo: 2 lanches de R$ 25 + R$ 2 de adicional por lanche
# (2500 + 200) × 2 = 5400; entrega 500; total 5900.
# Se houver desconto por quantidade de 400 e cupom de 500:
# 5400 - 400 + 500 - 500 = 5000 (R$ 50,00).Para conferir faturamento, considere apenas pedidos COMPLETED. NEW é um pedido pendente; CANCELLED e EXPIRED não representam venda concluída.
6. Evite duplicações e pedidos perdidos
- Ative o recebimento antes da carga inicial. Armazene os eventos enquanto consulta os pedidos existentes pela API; depois processe ambos com a mesma regra de atualização.
- Grave antes de confirmar. Receba o webhook, valide a assinatura e persista o evento em uma fila durável com id único. Só então responda 2xx. Uma repetição já persistida também deve receber 2xx.
- Atualize pelo id do pedido. Webhooks podem chegar repetidos e fora de ordem. Compare updated_at atomicamente no banco; não substitua um pedido por uma versão mais antiga. Em um empate com conteúdo diferente, consulte GET /orders/{orderId} para reconciliar o estado atual.
- Salve o cursor após processar a página. Se a importação falhar, repita a mesma página. Evite o filtro de status numa sincronização geral: filtrar NEW, por exemplo, não informa que um pedido passou para COMPLETED.
- Reconcilie periodicamente. Recomece uma consulta separada com updated_since anterior ao último horário sincronizado e percorra todas as páginas. Uma janela de sobreposição (por exemplo, 1 hora) ajuda a recuperar transações demoradas; ajuste ao seu cenário e faça verificações mais amplas. Não envie o cursor anterior ao iniciar essa reconciliação.
A API retorna estado atual, não um histórico de eventos. A espera de 5 segundos reduz, mas não elimina, o risco de transações que confirmam tarde. Não dependa apenas de avançar o cursor para sempre.
7. Resolver problemas
{
"error": "Chave de API inválida.",
"code": "UNAUTHORIZED",
"reqId": "identificador-da-requisicao"
}| Campo / valor | Como usar |
|---|---|
| 400 · BAD_REQUEST | Confira datas com fuso, limit entre 1 e 100, status e cursor completo. Parâmetros desconhecidos são recusados. |
| 401 · UNAUTHORIZED | Confira Authorization: Bearer e o valor completo da chave czk_. Chaves revogadas também retornam 401. |
| 404 · NOT_FOUND | Use o id do pedido (não code) e uma chave da mesma loja. |
| 429 · RATE_LIMITED | Espere o número de segundos do cabeçalho Retry-After antes de tentar novamente. Reduza a frequência de consultas. |
| 500 · INTERNAL | Tente novamente com espera progressiva. Guarde reqId, quando presente, para informar ao suporte; não registre credenciais nem dados pessoais. |
| Assinatura inválida | Use o segredo whsec_ correto, corpo bruto sem reserializar, timestamp do cabeçalho e relógio sincronizado. Confira se o segredo foi trocado. |
| Webhook não chega | Confira se está ativo, se a URL é HTTPS pública sem redirecionamento e se firewall/proxy aceita POST. Veja o erro e o HTTP no histórico de entregas. |
| Teste funciona, mas não há pedido | ping só testa conectividade e assinatura. Faça um pedido pelo cardápio para receber order.created. |
8. Valide antes de colocar em uso
- Consulte a API com uma chave válida e confira a resposta 401 com uma chave inválida.
- Receba o ping e depois um pedido real de teste; confirme o mesmo id na API e no webhook.
- Confira adicionais, combos, promoções, cupom e taxa de entrega, incluindo o total em centavos.
- Teste retirada e mesa, quando habilitadas, com os campos opcionais ausentes.
- Conclua ou cancele um pedido no painel e confirme a atualização no seu sistema.
- Faça o receptor responder 500 em um teste controlado e confira o reenvio sem duplicar o pedido. Teste também eventos fora de ordem.
- Percorra mais de uma página com limit=1 e retome uma consulta após uma resposta vazia.
- Interrompa e retome seu integrador, verificando a fila persistente, o cursor salvo e a reconciliação.
Os exemplos usam dados fictícios. Faça os testes em uma loja destinada à validação ou combine os pedidos de teste com o lojista: os endpoints usam os dados reais da loja vinculada à chave.