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.

  1. No painel da loja, abra Integrações → Chaves de API.
  2. Crie uma chave com o nome do seu sistema e copie o valor completo. Ele aparece apenas uma vez.
  3. No terminal do seu servidor, defina as variáveis abaixo e execute a consulta.
Primeira consulta · cURL
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.

Escolha a forma de integrar
Campo / valorComo usar
APISeu sistema busca pedidos periodicamente. É suficiente para começar e não exige uma URL para receber eventos.
Webhook + APIReceba 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.

Parâmetros da listagem
Campo / valorComo usar
updated_sinceData ISO 8601 com fuso, por exemplo 2026-09-21T00:00:00Z. Retorna alterações estritamente posteriores à data.
cursorValor de next_cursor da chamada anterior. Trate como texto opaco: não monte nem decodifique. Tem precedência sobre updated_since.
limitInteiro de 1 a 100; padrão 50. Mantenha o limite nas próximas páginas.
statusOpcional: NEW, COMPLETED, CANCELLED, EXPIRED, CONFIRMED, IN_PREPARATION, READY ou OUT_FOR_DELIVERY. Mantenha o filtro nas próximas páginas.
Consulta com data e próxima página · cURL
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'
Resposta da listagem · JSON com pedido completo
{
  "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}

Consultar um pedido · cURL
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

  1. Disponibilize um endpoint POST em uma URL HTTPS pública, por exemplo https://seu-sistema.com/webhooks/cardzap.
  2. Em Integrações → Webhook, salve a URL e mantenha o webhook ativo.
  3. Copie o segredo de assinatura para CARDZAP_WEBHOOK_SECRET no servidor receptor.
  4. 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.

Cabeçalhos do webhook
Campo / valorComo usar
Content-Typeapplication/json
CardZap-EventTipo do evento, também disponível em type no corpo.
CardZap-DeliveryIdentificador da entrega para diagnóstico. Para evitar duplicações, use id do corpo.
CardZap-Signaturet=<segundos Unix>,v1=<HMAC-SHA256 em hexadecimal>. A assinatura cobre t + ponto + corpo bruto e é renovada em cada tentativa.
Eventos disponíveis
Campo / valorComo usar
order.createdPedido criado, normalmente com status NEW.
order.completedPedido concluído (COMPLETED).
order.cancelledPedido cancelado (CANCELLED).
order.expiredPedido expirado (EXPIRED).
order.updatedMudança em etapas de atendimento de pedidos legados.
pingTeste de conexão. data contém restaurant_id; não contém order. Não há retentativa automática do ping.
Evento de pedido · JSON
{
  "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+
// 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"));
Iniciar o receptor
export CARDZAP_WEBHOOK_SECRET='whsec_SUBSTITUA_PELO_SEGREDO'
node receptor.mjs

Valide 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.

Campos do pedido
Campo / valorComo usar
id / restaurant_idIdentificadores do pedido e da loja. Use id como chave de atualização no seu sistema.
number / codeNúmero sequencial por loja e código para exibição (ex.: 42 / 0042). Pedidos antigos podem ter number null; code tem um fallback.
statusFluxo 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_atCriação e última alteração. Use updated_at para impedir que um estado antigo substitua um mais recente.
completed_at / cancelled_at / expired_atData da respectiva transição, ou null.
fulfillment / fulfillment_labelDELIVERY (entrega), PICKUP (retirada) ou DINE_IN (consumo na mesa); fulfillment_label é o texto de exibição.
table_labelIdentificação da mesa registrada no pedido, ou null. Pedidos de mesa não expiram automaticamente.
customername (texto) e phone (texto ou null). DINE_IN não coleta telefone; não exija telefone para importar.
address / notesEndereço e observações, ou null. Endereço pode estar ausente em retirada ou mesa.
paymentmethod: 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.
itemsLinhas do carrinho, incluindo itens e combos. Veja os campos abaixo.
promotions / promo_discount_centsDescontos 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_centsSubtotal das linhas antes dos descontos por quantidade e cupom; taxa de entrega.
couponObjeto com code e discount_cents, ou null.
total_centsValor final calculado pelo CardZap. Use este valor ao importar o pedido.
Campos de cada linha em items
Campo / valorComo usar
kind / item_id / combo_idkind é item ou combo. Em combos, item_id é null e combo_id identifica o combo; em itens, combo_id é null.
name / external_codeNome 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_centsQuantidade da linha e preço unitário cobrado, sem adicionais. Um preço promocional já está refletido aqui.
regular_unit_price_centsPreço unitário original quando registrado para a promoção, ou null. Não é um desconto extra a subtrair.
optionsAdicionais: name, external_code (ou null) e price_cents por unidade da linha. Multiplique pelo quantity da linha; não há quantity em cada adicional.
componentsConteú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.
Conferência dos valores
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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

Formato de erro da API · JSON
{
  "error": "Chave de API inválida.",
  "code": "UNAUTHORIZED",
  "reqId": "identificador-da-requisicao"
}
Erros e soluções
Campo / valorComo usar
400 · BAD_REQUESTConfira datas com fuso, limit entre 1 e 100, status e cursor completo. Parâmetros desconhecidos são recusados.
401 · UNAUTHORIZEDConfira Authorization: Bearer e o valor completo da chave czk_. Chaves revogadas também retornam 401.
404 · NOT_FOUNDUse o id do pedido (não code) e uma chave da mesma loja.
429 · RATE_LIMITEDEspere o número de segundos do cabeçalho Retry-After antes de tentar novamente. Reduza a frequência de consultas.
500 · INTERNALTente novamente com espera progressiva. Guarde reqId, quando presente, para informar ao suporte; não registre credenciais nem dados pessoais.
Assinatura inválidaUse 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 chegaConfira 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á pedidoping 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.