API de Integração Drezzo
Referência técnica para integradores. Versão 1.0 ·
https://api.drezzopay.com/v1
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.
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.
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
| Tipo | Formato | Exemplo |
|---|---|---|
| Parâmetro de data | YYYY-MM-DD | 2026-07-20 |
| Timestamp em resposta | ISO 8601 com offset | 2026-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ção | Resultado |
|---|---|
| Mesma rota, mesmos filtros | ✅ funciona |
Trocar limit no meio | ✅ permitido |
| Ordem diferente dos parâmetros | ✅ irrelevante |
Cursor de /v1/sales em /v1/returns | ❌ 400 invalid_cursor |
Trocar eventId, storeId ou qualquer filtro | ❌ 400 invalid_cursor |
| Cursor editado à mão | ❌ 400 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.", …}}
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
| Escopo | Limite |
|---|---|
| Requisições por token | 120 / min |
| Janela por consulta | 92 dias |
| Registros por página | 500 |
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:
| Campo | Onde |
|---|---|
returnedQuantity | items[] |
returnedAmountCents | items[] e totals |
serviceFee.reversedAmountCents | venda |
totals.returnedServiceFeeAmountCents | totals |
totals.totalAmountCents | totals |
/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
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
| Parâmetro | Descrição |
|---|---|
storeId | Obrigatório se o token cobrir mais de uma loja |
from, to | Janela de datas |
customerDocument | Só os eventos em que esse consumidor esteve (pelo check-in) |
cursor, limit | Paginação |
| Campo | Tipo | Descrição |
|---|---|---|
id | string | Identificador do evento |
name | string | Nome |
date | string | Data (YYYY-MM-DD) |
status | enum | scheduled, open, closed |
totalAmountCents | int | Faturamento |
customerCount | int | Consumidores |
productsSold | int | Itens vendidos |
Vendas
endpoint principal Uma venda vem completa: itens, consumidor, taxa de serviço e totais. Não é preciso juntar endpoints para montar o registro.
| Parâmetro | Descrição |
|---|---|
storeId | Obrigatório se a chave cobrir mais de uma loja |
eventId | Filtra por evento |
customerDocument | Filtra pelo documento do consumidor (CPF/CNPJ, com ou sem máscara) |
from, to | Janela sobre a data do evento |
createdSince | Sincronização incremental (ISO 8601) |
cursor, limit | Paginaçã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 camposreturned*: 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
customerpode sernull. Venda a consumidor não identificado.- O consumidor é identificado pelo
document. -
customer.emailé frequentementenull: só é capturado no cadastro pelo aplicativo; quem entra pelo check-in no PDV costuma não fornecer. Trate a ausência sem falhar. serviceFeeénullquando não há taxa.RateBpsem basis points (1000 = 10%).- A taxa de serviço nunca está embutida em
unitPriceCentsnem nos totais de mercadoria.
Estornos
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.
| Campo | O 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.
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
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.
| Campo | Tipo | Descrição |
|---|---|---|
id, sku, name | string | Identificação. Sku pode ser nulo. |
categoryId, categoryName | string | Categoria |
priceCents | int | Preço atual |
unit | string | Unidade comercial |
type | enum | Ver tabelas |
active | bool | Ativo |
updatedAt | string | Última alteração |
id, não pela presença no catálogo.
Consumidores
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âmetro | Descrição |
|---|---|
document | Busca exata por documento (CPF/CNPJ, com ou sem máscara) |
updatedSince | Sincronização incremental |
cursor, limit | Paginação |
| Campo | Tipo | Descrição |
|---|---|---|
document | string | Identificador do consumidor. Apenas dígitos quando CPF/CNPJ. |
name | string | Nome |
documentType | enum | CPF, CNPJ, PASSPORT, NONE |
email, phone | string | Contato. Email frequentemente nulo. |
birthDate | string | Nascimento |
updatedAt | string | Última alteração |
Tabelas de referência
Meios de pagamento paymentMethod
| Valor | Descrição |
|---|---|
cash | Dinheiro |
credit | Cartão de crédito |
debit | Cartão de débito |
pix | PIX |
prepaid | Debitado de saldo pré-carregado |
postpaid | Conta pós-paga |
voucher | Vale-refeição |
comp | Cortesia (valor pago zero) |
other | Outros |
/v1/sales com
paymentMethod: "prepaid", indicando que o dinheiro entrou antes, na recarga.
Não há endpoint de recargas.
Tipos de produto type
| Valor | Descrição |
|---|---|
product | Mercadoria |
entrance | Entrada / couvert |
combo | Combo |
supply | Insumo (não vendido diretamente) |
configurable | Produto montável |
Status de evento status
| Valor | Descrição |
|---|---|
scheduled | Agendado, ainda não iniciado |
open | Em operação. Os agregados do evento ainda mudam. As vendas em /v1/sales já são finais. |
closed | Encerrado. Agregados do evento finais |
Erros
{
"error": {
"code": "invalid_parameter",
"message": "`from` deve estar no formato YYYY-MM-DD.",
"field": "from",
"requestId": "req_01HQ8XZ4K9M2N7P3"
}
}
| Status | code | Significado |
|---|---|---|
400 | invalid_parameter | Parâmetro ausente, desconhecido ou malformado |
400 | invalid_cursor | Cursor de outra rota ou de outros filtros |
400 | store_required | storeId omitido em token multi-loja |
401 | unauthorized | Token ausente, inválido ou revogado |
403 | forbidden | Sem acesso ao recurso |
404 | not_found | Recurso inexistente |
422 | unprocessable | Semanticamente inválido (ex.: to antes de from) |
429 | rate_limited | Limite excedido. Respeite o Retry-After |
500 | internal_error | Erro 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
- Autenticar e listar lojas.
- Percorrer um evento completo com paginação por cursor até
hasMore: false. - Conferir as identidades de valores em todas as vendas do evento.
- Guardar o
syncCursor, rodar de novo sem nada novo e confirmar que a resposta vem vazia. - Interromper uma varredura entre páginas e retomá-la pelo cursor, confirmando que nenhum registro se perde.
- Reprocessar a última página e confirmar que o seu lado é idempotente por
id. - Usar um cursor de
/v1/salesem/v1/returnse confirmar400 invalid_cursor. - Registrar um estorno parcial e confirmar que ele aparece em
/v1/returns. É o teste que mais importa. - Tratar
429com backoff exponencial. - Confirmar que uma venda offline traz
occurredAtanterior acreatedAt.
Primeira chamada
curl -H "Authorization: Bearer dz_live_…" \
https://api.drezzopay.com/v1/stores