API de Integração Drezzo

Referência técnica para integradores. Versão 1.0 · https://api.drezzopay.com/v1

Para experimentar sem escrever código, use o console de testes: cole o token, escolha a rota, preencha os campos e dispare. Ele também mostra o curl equivalente.

Visão geral

A API expõe os dados comerciais de um estabelecimento. Vendas, itens, consumidores, estornos, catálogo e eventos, para consumo por sistemas externos.

Todos os endpoints são GET. A API é somente leitura.

A API não guarda estado do seu lado. Não há campo que marque um registro como “já processado”, nem endpoint para você escrever de volta. O controle do que já foi lido e tratado é seu. Chaveie pelo id da venda, que é estável e permanente.

Fluxo típico

1. GET /v1/sales                    (na 1ª vez, sem cursor)
2. percorra TODAS as páginas usando nextCursor
3. ao terminar, guarde o syncCursor da última resposta
4. na próxima execução, envie ?cursor=<syncCursor>
5. repita o mesmo processo em /v1/returns

Uma venda fica disponível assim que é registrada no PDV. Não é preciso esperar o evento acabar. Ver Sincronização para a semântica completa.

Autenticação

Authorization: Bearer dz_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Formato do token:

dz_<ambiente>_<32 hexadecimais>
     │
     ├── live    produção
     └── test    sandbox

O token é vinculado a uma ou mais lojas e determina o que pode ser lido. Pedir uma loja fora do escopo retorna 403.

Tokens são gerados pelo estabelecimento no Dashboard Drezzo, em Configurações → Chaves de API. A Drezzo guarda apenas o hash: um token perdido não pode ser recuperado, apenas revogado e substituído.

Nunca envie o token em query string, grave em log ou versione em repositório. Ele dá acesso de leitura a todas as vendas da loja.

Convenções

Valores monetários

Todo valor monetário é inteiro, em centavos, e o nome do campo sempre termina em Cents. Não existe campo de dinheiro com casa decimal nesta API.

{ "unitPriceCents": 1500 }    // R$ 15,00
{ "totalAmountCents": 3300 }  // R$ 33,00

Datas e horários

TipoFormatoExemplo
Parâmetro de dataYYYY-MM-DD2026-07-20
Timestamp em respostaISO 8601 com offset2026-07-20T23:15:42-03:00

O filtro from/to opera sobre a data do EVENTO, não sobre o horário da transação. Uma venda às 03:40 do dia 21/07, pertencente ao evento do dia 20/07, é retornada em from=2026-07-20&to=2026-07-20. Eventos noturnos viram a madrugada, e é este o comportamento correto para conciliação.

occurredAt é a hora real da venda, não a do registro. O PDV opera offline e sincroniza depois; nessas vendas createdAt (hora do sync) é posterior. Já vimos diferenças de mais de 2 horas em produção. Use occurredAt como a hora da venda.

Intervalos são inclusivos nas duas pontas. Janela máxima por consulta: 92 dias.

Paginação por cursor

{
  "data": [ … ],
  "pagination": { "nextCursor": "eyJ0IjoiMjAyNi0…", "hasMore": true }
}

Repita a chamada passando cursor=<nextCursor> até hasMore ser false. Limit aceita até 500 (padrão 200).

Não usamos offset de propósito: numa varredura em que linhas novas chegam entre páginas, offset pula registros, e um registro pulado nunca chega ao seu sistema. O cursor ancora na última linha lida.

Contrato do cursor

O cursor é opaco e vinculado à rota e aos filtros da consulta que o gerou. Durante a paginação, mantenha os mesmos filtros e altere apenas cursor e, se quiser, limit.

SituaçãoResultado
Mesma rota, mesmos filtros✅ funciona
Trocar limit no meio✅ permitido
Ordem diferente dos parâmetros✅ irrelevante
Cursor de /v1/sales em /v1/returns400 invalid_cursor
Trocar eventId, storeId ou qualquer filtro400 invalid_cursor
Cursor editado à mão400 invalid_cursor

O cursor não expira. Pode ser guardado por dias e retomado. Não é assinado criptograficamente, e não precisa ser: ele só escolhe posição dentro de um escopo que o token já autorizou, então adulterá-lo não dá acesso a nada.

400 {"error":{"code":"invalid_cursor",
     "message":"O cursor não pertence a esta rota ou não corresponde aos
                filtros informados.",
     "field":"cursor","requestId":"req_…"}}

Escopo da loja

Nas rotas que aceitam storeId, ele é opcional quando o token cobre exatamente uma loja e obrigatório quando cobre duas ou mais. Sua ausência no segundo caso retorna 400 store_required; pedir uma loja fora do escopo do token retorna 403.

GET /v1/sales/{id} não aceita storeId: o identificador da venda já é único, e o escopo sai das lojas do token.

Parâmetros são validados

Parâmetro de query não reconhecido devolve 400, com o nome do que não foi entendido e a lista do que aquela rota aceita.

GET /v1/sales?customerId=8891

400 {"error":{"code":"invalid_parameter",
     "message":"Parâmetro não reconhecido: customerId. Aceitos nesta rota:
                storeId, eventId, customerDocument, from, to,
                createdSince, cursor, limit.", …}}
Isso é deliberado. Se aceitássemos e ignorássemos, quem digitasse o nome errado de um filtro receberia 200 com a lista inteira, acharia que filtrou e processaria dado de outro consumidor sem nunca perceber.

Busca por consumidor

O consumidor é identificado pelo documento. Não expomos id interno de consumidor em lugar nenhum da API.

GET /v1/customers?document=12345678900              o consumidor
GET /v1/sales?customerDocument=12345678900          as compras dele
GET /v1/sales?customerDocument=…&eventId=1001       as compras dele num evento
GET /v1/events?customerDocument=12345678900         os eventos em que esteve

Aceita com ou sem máscara. 123.456.789-00 e 12345678900 chegam ao mesmo lugar. Documento desconhecido devolve lista vazia, nunca 404.

Limites

EscopoLimite
Requisições por token120 / min
Janela por consulta92 dias
Registros por página500

Ao exceder, retornamos 429 com Retry-After. Use backoff exponencial.

Compatibilidade

Mudanças aditivas (novos campos, novos endpoints, novos valores de enum) acontecem sem aviso na v1. Ignore campos desconhecidos e tenha um caso default para valores de enum não previstos. Mudanças incompatíveis ganham nova versão, com no mínimo 6 meses em paralelo.

Sincronização

O que é definitivo e o que é projeção

Os dados originais da venda são definitivos assim que ela é registrada: itens, quantidades, preços, descontos, forma de pagamento e valores cobrados não são alterados.

Alguns campos são projeções calculadas no momento da leitura e podem mudar quando novos estornos forem registrados:

CampoOnde
returnedQuantityitems[]
returnedAmountCentsitems[] e totals
serviceFee.reversedAmountCentsvenda
totals.returnedServiceFeeAmountCentstotals
totals.totalAmountCentstotals

/v1/sales é append-only para fins de descoberta: uma venda aparece uma única vez no stream incremental. Reconsultar a mesma venda pode devolver projeções de estorno atualizadas.

Retomada: use o syncCursor

Toda resposta de lista traz um syncCursor. A posição exata da última linha entregue. Guarde-o ao terminar a varredura e envie em cursor na próxima execução. A comparação é estrita, então retomar por ele é exatamente-uma-vez: sem lacuna e sem repetição.

GET /v1/sales?storeId=6
→ { "data": [...],
    "pagination": { "nextCursor": "eyJ0Ijo…", "hasMore": true },
    "syncCursor": "eyJ0Ijo…" }

  … percorra até hasMore: false …

→ { "data": [...],
    "pagination": { "nextCursor": null, "hasMore": false },
    "syncCursor": "eyJ0IjoiMjAyNi0…" }   ← guarde ESTE

próxima execução:
GET /v1/sales?storeId=6&cursor=eyJ0IjoiMjAyNi0…

Não use relógio como marca d'água. Guardar "o horário em que a sincronização rodou" perde registros: uma venda criada às 22:00:02 pode não entrar no snapshot lido às 22:00:00, e se você guardar 22:00:10 ela nunca mais aparece.

O syncCursor ancora numa linha, não num instante, por isso não tem esse buraco. Se você usar createdSince (que é inclusivo: createdAt >= createdSince), guarde o maior createdAt efetivamente recebido, nunca o horário da consulta, e deduplique por id.

Mesmo com o cursor, trate o processamento como idempotente por id. Uma queda entre "processei" e "salvei o cursor" faz a página anterior ser lida de novo.

Estorno é registro novo, não alteração da venda

Quando parte de uma venda é devolvida, a venda original não muda. Um registro novo aparece em /v1/returns.

Uma venda estornada não reaparece em /v1/sales.

Veja o que acontece:

22:00  venda 12345 criada (2 chopes, R$ 30,00 + taxa R$ 3,00)
       você lê /v1/sales e processa. tudo certo.

23:30  operador estorna 1 chope dessa venda

23:35  você lê /v1/sales?createdSince=22:00
       -> a venda 12345 NÃO aparece.
          ela não foi alterada. o estorno é registro separado.

       você lê /v1/returns?createdSince=22:00
       -> aqui está: 1 unidade
          mercadoria R$ 15,00 + taxa R$ 1,50 = R$ 16,50

Reler a venda 12345 diretamente mostra returnedQuantity e returnedAmountCents preenchidos. São projeções do momento da leitura. Só que nada te avisa que é preciso relê-la: ela não aparece em varredura incremental nenhuma. E mesmo relendo, os campos da venda são o acumulado; a parcela de cada estorno só existe em /v1/returns.

Por isso: percorra os dois streams. Vendas em /v1/sales, estornos em /v1/returns.

Estorno é parcial por item: quantity pode ser menor que a quantidade vendida. No exemplo acima, 1 chope de 2. Use refundAmountCents como o valor da devolução, nunca o total da venda.

Evento aberto não é problema

Você pode processar vendas de um evento em andamento. A venda é final no instante em que é registrada

O que continua se movendo enquanto o evento está open são apenas os agregados do evento em /v1/events: totalAmountCents, customerCount, productsSold. Se o seu sistema faz conferência ou fechamento contra esses números, aí sim espere status: "closed". Para as vendas em si. Não há espera.

Lojas

GET /v1/stores

Lojas que a chave enxerga. Sem parâmetros. Loja é o estabelecimento com CNPJ próprio. Os pontos de venda (bares) compartilham o CNPJ da loja e não são entidades separadas.

{
  "data": [{
    "id": "6",
    "name": "Drezzo Bar",
    "document": "12345678000190",
    "timezone": "America/Sao_Paulo",
    "posLocations": [
      { "id": "12", "name": "Bar Central" }
    ]
  }]
}

Eventos

GET /v1/events?storeId={id}&from={data}&to={data}
ParâmetroDescrição
storeIdObrigatório se o token cobrir mais de uma loja
from, toJanela de datas
customerDocumentSó os eventos em que esse consumidor esteve (pelo check-in)
cursor, limitPaginação
CampoTipoDescrição
idstringIdentificador do evento
namestringNome
datestringData (YYYY-MM-DD)
statusenumscheduled, open, closed
totalAmountCentsintFaturamento
customerCountintConsumidores
productsSoldintItens vendidos

Vendas

GET /v1/sales?storeId={id}&eventId={id}
GET /v1/sales/{id}

endpoint principal Uma venda vem completa: itens, consumidor, taxa de serviço e totais. Não é preciso juntar endpoints para montar o registro.

ParâmetroDescrição
storeIdObrigatório se a chave cobrir mais de uma loja
eventIdFiltra por evento
customerDocumentFiltra pelo documento do consumidor (CPF/CNPJ, com ou sem máscara)
from, toJanela sobre a data do evento
createdSinceSincronização incremental (ISO 8601)
cursor, limitPaginação
{
  "data": [{
    "id": "12345",
    "storeId": "6",
    "orderNumber": 42,
    "event": { "id": "1001", "name": "Réveillon 2026", "date": "2026-07-20" },
    "occurredAt": "2026-07-20T23:15:42-03:00",
    "posLocation": { "id": "12", "name": "Bar Central" },
    "operator": { "id": "44", "name": "Maria Souza" },
    "customer": {
      "name": "João Silva",
      "document": "12345678900",
      "documentType": "CPF",
      "email": "joao@email.com",
      "phone": "11987654321"
    },
    "paymentMethod": "prepaid",
    "items": [{
      "id": "77001",
      "productId": "98765",
      "sku": "CHOPP300",
      "name": "Chopp 300ml",
      "categoryId": "5",
      "categoryName": "Chopes",
      "quantity": 2,
      "unit": "UN",
      "unitPriceCents": 1500,
      "grossAmountCents": 3000,
      "discountAmountCents": 0,
      "netAmountCents": 3000,
      "returnedQuantity": 0,
      "returnedAmountCents": 0
    }],
    "serviceFee": {
      "rateBps": 1000,
      "amountCents": 300,
      "reversedAmountCents": 0,
      "waived": false
    },
    "totals": {
      "grossAmountCents": 3000,
      "discountAmountCents": 0,
      "netAmountCents": 3000,
      "serviceFeeAmountCents": 300,
      "returnedAmountCents": 0,
      "returnedServiceFeeAmountCents": 0,
      "totalAmountCents": 3300
    },
    "createdAt": "2026-07-20T23:15:42-03:00"
  }],
  "pagination": { "nextCursor": "eyJ0IjoiMjAyNi0…", "hasMore": true }
}

Identidades garantidas

netAmountCents   = grossAmountCents − discountAmountCents
totalAmountCents = netAmountCents + serviceFeeAmountCents
                   − returnedAmountCents − returnedServiceFeeAmountCents

E, dentro de uma venda, sum(items[].discountAmountCents) fecha exatamente com totals.discountAmountCents. O desconto é registrado por venda e rateado por item proporcionalmente ao valor, com o resto do arredondamento no item de maior valor.

A taxa de serviço também é estornada. Quando parte de uma venda é devolvida, a taxa é revertida na mesma proporção. Dois campos descrevem isso:

  • serviceFee.amountCents. A taxa cobrada na venda. Não muda.
  • serviceFee.reversedAmountCents. Quanto já foi revertido. É projeção, igual aos campos returned*: reflete o momento da leitura e pode crescer depois.

Para processar a devolução, use /v1/returns: cada estorno traz refundAmountCents (mercadoria) e serviceFeeRefundAmountCents (taxa), já somados em totalRefundAmountCents. Os campos aqui na venda são o acumulado, útil para conferência.

Observações de campo

Estornos

GET /v1/returns?storeId={id}&createdSince={timestamp}

Stream append-only de devoluções. É o canal para detectar estorno.

{
  "data": [{
    "id": "551",
    "saleId": "12345",
    "storeId": "6",
    "eventId": "1001",
    "productId": "98765",
    "sku": "CHOPP300",
    "productName": "Chopp 300ml",
    "quantity": 1,
    "refundAmountCents": 1500,
    "serviceFeeRefundAmountCents": 150,
    "totalRefundAmountCents": 1650,
    "reason": "desistencia_com_estoque",
    "occurredAt": "2026-07-21T01:02:00-03:00"
  }],
  "pagination": { "nextCursor": null, "hasMore": false }
}

Case saleId com o registro que você já processou e trate a devolução por refundAmountCents. Um mesmo produto pode ter mais de um estorno na mesma venda.

CampoO que é
refundAmountCents Valor devolvido referente aos itens, já considerando o desconto atribuído às unidades devolvidas. Não inclui a taxa.
serviceFeeRefundAmountCents Parcela da taxa de serviço revertida por este estorno.
totalRefundAmountCents A soma dos dois. O valor total desta devolução.
totalRefundAmountCents = refundAmountCents + serviceFeeRefundAmountCents

Cada estorno carrega a sua própria parcela de taxa, então não é preciso reler a venda para saber quanto voltou. Com dois estornos na mesma venda, cada linha traz o que lhe corresponde.

Ao usar eventId, uma página pode vir com menos itens que limit, ou vazia, ainda com hasMore: true. Nenhum registro se perde: percorra até hasMore ser false, como em qualquer outra rota.

Produtos

GET /v1/products?storeId={id}&updatedSince={timestamp}

Catálogo dos produtos da loja. Se o seu sistema precisa de atributos próprios por produto (classificação, tributação, códigos internos), mantenha-os do seu lado chaveados por id ou sku.

CampoTipoDescrição
id, sku, namestringIdentificação. Sku pode ser nulo.
categoryId, categoryNamestringCategoria
priceCentsintPreço atual
unitstringUnidade comercial
typeenumVer tabelas
activeboolAtivo
updatedAtstringÚltima alteração
Produtos excluídos somem do catálogo mas continuam aparecendo em vendas antigas. Chaveie pelo id, não pela presença no catálogo.

Consumidores

GET /v1/customers?storeId={id}&document={documento}
GET /v1/customers?storeId={id}&updatedSince={timestamp}

Cadastro de consumidores da loja. O consumidor já vem embutido em cada venda de /v1/sales. Este endpoint serve para buscar por documento e para sincronizar dado mestre.

ParâmetroDescrição
documentBusca exata por documento (CPF/CNPJ, com ou sem máscara)
updatedSinceSincronização incremental
cursor, limitPaginação
Consumidor de outra loja não é encontrado. O cadastro é global na Drezzo, mas a busca é sempre restrita às lojas da sua chave. Buscar um documento que só existe em outro estabelecimento devolve lista vazia, não o consumidor.
CampoTipoDescrição
documentstringIdentificador do consumidor. Apenas dígitos quando CPF/CNPJ.
namestringNome
documentTypeenumCPF, CNPJ, PASSPORT, NONE
email, phonestringContato. Email frequentemente nulo.
birthDatestringNascimento
updatedAtstringÚltima alteração

Tabelas de referência

Meios de pagamento paymentMethod

ValorDescrição
cashDinheiro
creditCartão de crédito
debitCartão de débito
pixPIX
prepaidDebitado de saldo pré-carregado
postpaidConta pós-paga
voucherVale-refeição
compCortesia (valor pago zero)
otherOutros
Recarga não é venda. No pré-pago, o consumidor carrega saldo e depois consome contra ele. A recarga é aporte de crédito, não a saída de um produto. Esta API expõe o consumo: cada venda aparece em /v1/sales com paymentMethod: "prepaid", indicando que o dinheiro entrou antes, na recarga. Não há endpoint de recargas.

Tipos de produto type

ValorDescrição
productMercadoria
entranceEntrada / couvert
comboCombo
supplyInsumo (não vendido diretamente)
configurableProduto montável

Status de evento status

ValorDescrição
scheduledAgendado, ainda não iniciado
openEm operação. Os agregados do evento ainda mudam. As vendas em /v1/sales já são finais.
closedEncerrado. Agregados do evento finais

Erros

{
  "error": {
    "code": "invalid_parameter",
    "message": "`from` deve estar no formato YYYY-MM-DD.",
    "field": "from",
    "requestId": "req_01HQ8XZ4K9M2N7P3"
  }
}
StatuscodeSignificado
400invalid_parameterParâmetro ausente, desconhecido ou malformado
400invalid_cursorCursor de outra rota ou de outros filtros
400store_requiredstoreId omitido em token multi-loja
401unauthorizedToken ausente, inválido ou revogado
403forbiddenSem acesso ao recurso
404not_foundRecurso inexistente
422unprocessableSemanticamente inválido (ex.: to antes de from)
429rate_limitedLimite excedido. Respeite o Retry-After
500internal_errorErro interno; pode ser repetido

Cite sempre o requestId ao reportar um problema. Ele localiza a requisição exata nos logs da Drezzo.

Checklist de homologação

  1. Autenticar e listar lojas.
  2. Percorrer um evento completo com paginação por cursor até hasMore: false.
  3. Conferir as identidades de valores em todas as vendas do evento.
  4. Guardar o syncCursor, rodar de novo sem nada novo e confirmar que a resposta vem vazia.
  5. Interromper uma varredura entre páginas e retomá-la pelo cursor, confirmando que nenhum registro se perde.
  6. Reprocessar a última página e confirmar que o seu lado é idempotente por id.
  7. Usar um cursor de /v1/sales em /v1/returns e confirmar 400 invalid_cursor.
  8. Registrar um estorno parcial e confirmar que ele aparece em /v1/returns. É o teste que mais importa.
  9. Tratar 429 com backoff exponencial.
  10. Confirmar que uma venda offline traz occurredAt anterior a createdAt.

Primeira chamada

curl -H "Authorization: Bearer dz_live_…" \
  https://api.drezzopay.com/v1/stores