Caixa — visão geral
O caixa é uma conta operacional (operator_type=cashier), criada pela
gestão e escopada a um workspace específico — ver Gestão → Operadores (PDV
e caixa).
Autenticação
| Endpoint | O que faz |
|---|---|
POST /v1/auth/cashier/login | Login com username/senha gerados pela gestão. Seta wallet_cashier_access_token, wallet_cashier_refresh_token, wallet_cashier_csrf_token. |
POST /v1/auth/cashier/refresh | Rotaciona o refresh token. |
POST /v1/auth/cashier/logout | Revoga a sessão e limpa os cookies wallet_cashier_*. |
O JWT do caixa segue o mesmo formato do PDV — troque operator_type para
"cashier". Veja PDV → visão geral para o exemplo
completo das claims.
Localizar ou cadastrar um participante
Duas rotas globais (wallet-control-api), sem eventId porque identidade é
global — o saldo/wallet fica na região:
| Endpoint | O que faz |
|---|---|
GET /v1/cashier/participants/by-phone/{phone} | Consulta pura, nunca cria nada. 404 PARTICIPANT_NOT_FOUND se o telefone não tem cadastro ainda. |
POST /v1/cashier/participants | Cria-ou-localiza pelo telefone. Se for novo, cria o perfil sem auth_user_id (a pessoa ainda não instalou o app nem fez OTP — isso acontece depois, sozinho, na primeira vez que ela confirmar o OTP do mesmo telefone). Sempre devolve uma activationAuthorization: uma autorização assinada, de uso único, válida só pra esse participante e pro evento do caixa. |
- curl
- JavaScript
- Python
curl -X POST http://127.0.0.1:8787/v1/cashier/participants \
-H "Authorization: Bearer <accessToken do caixa>" \
-H "Content-Type: application/json" \
-d '{ "phone": "+5511999999999" }'
const response = await fetch("http://127.0.0.1:8787/v1/cashier/participants", {
method: "POST",
headers: {
Authorization: `Bearer ${cashierAccessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ phone: "+5511999999999" }),
});
const { userId, displayName, profileComplete, activationAuthorization } = await response.json();
import requests
response = requests.post(
"http://127.0.0.1:8787/v1/cashier/participants",
headers={"Authorization": f"Bearer {cashier_access_token}"},
json={"phone": "+5511999999999"},
)
participant = response.json()
Consultar e ativar a wallet em nome do participante
Duas rotas regionais (wallet-api, /v1/events/{eventId}/...):
| Endpoint | O que faz |
|---|---|
GET /v1/events/{eventId}/cashier/wallets/{userId} | Consulta pura: saldo e status da wallet desse participante nesse evento. 404 WALLET_NOT_FOUND se ele ainda não ativou. |
POST /v1/events/{eventId}/cashier/wallets/activate | Cria a wallet, usando a activationAuthorization obtida no passo anterior. Idempotente. A autorização só pode ser usada uma vez — mesmo se a ativação falhar por outro motivo, ela é consumida; peça uma nova em caso de erro. |
GET /v1/events/{eventId}/cashier/wallets/{userId}/statement | Extrato paginado das movimentações dessa wallet (recargas hoje; vendas/estornos quando existirem). |
curl -X POST http://127.0.0.1:8787/v1/events/{eventId}/cashier/wallets/activate \
-H "Authorization: Bearer <accessToken do caixa>" \
-H "Content-Type: application/json" \
-d '{ "userId": "<userId devolvido no passo anterior>", "activationAuthorization": "<token devolvido no passo anterior>" }'
Extrato
curl "http://127.0.0.1:8787/v1/events/{eventId}/cashier/wallets/{userId}/statement?limit=20&sortDirection=desc" \
-H "Authorization: Bearer <accessToken do caixa>"
{
"items": [
{
"entryId": "018f26d7-...",
"transactionId": "018f26d7-...",
"type": "recharge",
"accountType": "WALLET_AVAILABLE",
"amountMinor": 5000,
"occurredAt": "2026-09-03T16:30:29.377Z",
"paymentMethod": "cash",
"note": null,
"createdByOperatorUsername": "CX-55130771",
"createdByOperatorDescription": "Entrada Principal"
}
],
"nextCursor": null
}
createdByOperatorUsername/createdByOperatorDescription identificam quem
fez a movimentação (o caixa ou, no futuro, o PDV) — vêm gravados junto com o
lançamento no momento em que ele acontece, então continuam mostrando quem
era o operador na hora, mesmo que a conta seja renomeada depois. Ambos são
null quando não há operador, ou quando a conta não tem descrição
cadastrada.
Paginado por cursor (não por página numerada): quando nextCursor vier
preenchido, chame de novo com ?cursor=<valor> pra pegar a próxima leva —
mantendo os mesmos sortDirection/filtros da chamada original, senão a API
rejeita com WALLET_STATEMENT_CURSOR_SORT_MISMATCH.
Filtros disponíveis, todos opcionais e combináveis:
| Parâmetro | O que faz |
|---|---|
type | Só um tipo de movimento (hoje só recharge existe). |
occurredAfter / occurredBefore | Intervalo de datas, ISO 8601, inclusivo dos dois lados. |
sortDirection | asc ou desc (padrão: desc, mais recente primeiro). |
limit | 1 a 100 (padrão 50). |
amountMinor vem com sinal — positivo é crédito (dinheiro entrando),
negativo é débito. paymentMethod/note só aparecem em lançamentos de
recarga; ficam null pra outros tipos de movimento no futuro.
Recarga
| Endpoint | O que faz |
|---|---|
POST /v1/events/{eventId}/cashier/topups | Recarrega a wallet do participante. Sempre imediata — não existe confirmação externa de pagamento nesta fase, o caixa só declara como o dinheiro entrou. |
paymentMethod é texto livre, não uma lista fechada — "cash",
"credit_card", "pix", ou qualquer categoria que a organização usar.
Exige o header Idempotency-Key: repetir a mesma chave devolve o mesmo
resultado, sem recreditar — mesmo que as duas chamadas cheguem ao mesmo
tempo, nunca executam a recarga em paralelo. Se uma segunda chamada com a
mesma chave chegar enquanto a primeira ainda está em andamento, ela é
rejeitada com 409 IDEMPOTENCY_KEY_IN_PROGRESS (em vez de esperar) — tente
de novo em seguida, não é um erro definitivo.
- curl
- JavaScript
- Python
curl -X POST http://127.0.0.1:8787/v1/events/{eventId}/cashier/topups \
-H "Authorization: Bearer <accessToken do caixa>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <uuid gerado pelo cliente>" \
-d '{ "userId": "<userId do participante>", "amountMinor": 5000, "paymentMethod": "cash" }'
const response = await fetch(`http://127.0.0.1:8787/v1/events/${eventId}/cashier/topups`, {
method: "POST",
headers: {
Authorization: `Bearer ${cashierAccessToken}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ userId, amountMinor: 5000, paymentMethod: "cash" }),
});
const { availableAmountMinor } = await response.json();
import requests
import uuid
response = requests.post(
f"http://127.0.0.1:8787/v1/events/{event_id}/cashier/topups",
headers={
"Authorization": f"Bearer {cashier_access_token}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"userId": user_id, "amountMinor": 5000, "paymentMethod": "cash"},
)
result = response.json()
Credencial QR impressa com PIN
Para participante sem celular/app:
| Endpoint | O que faz |
|---|---|
POST /v1/events/{eventId}/cashier/wallet-credentials/printed-qr | Emite uma credencial revogável ligada à wallet. O sistema gera um PIN aleatório e devolve no corpo da resposta uma única vez — não fica gravado em texto puro em lugar nenhum, só o hash. Reemitir revoga automaticamente a credencial anterior — nunca fica mais de uma ativa por wallet. |
Não existe endpoint de reset de PIN isolado, mantendo o mesmo QR. QR e PIN são dois fatores independentes — o PDV lê o QR pra achar a credencial, só depois pede o PIN pra validar aquela credencial específica. Um vazando sozinho não serve pra nada; só os dois juntos são um problema. Por isso, diante de qualquer suspeita (PIN visto, cartão sumiu, o que for), a resposta é sempre reemitir tudo — nunca dá pra ter certeza que só um dos dois vazou. Se for só o participante esquecendo o PIN, sem suspeita nenhuma de vazamento, também vale reemitir (é o único jeito disponível hoje).
O QR impresso ainda não pode ser usado pra pagar — o PDV ainda não tem o lado de leitura/consumo dessa credencial implementado.
Minhas próprias operações
GET /v1/events/{eventId}/cashier/operations — é o "extrato" acima, só que
ao contrário: em vez de todas as movimentações de uma wallet, é todas as
operações que você mesmo fez, em quantas wallets diferentes tiver
mexido. Sempre autoserviço — não existe parâmetro pra ver a atividade de
outro caixa, é sempre a sua própria, identificada pelo seu próprio token.
curl "http://127.0.0.1:8787/v1/events/{eventId}/cashier/operations?limit=20" \
-H "Authorization: Bearer <accessToken do caixa>"
{
"items": [
{
"transactionId": "018f26d7-...",
"type": "recharge",
"userId": "018f26d7-...",
"walletId": "018f26d7-...",
"amountMinor": 5000,
"occurredAt": "2026-09-03T16:30:29.377Z",
"paymentMethod": "cash",
"note": null,
"createdByOperatorUsername": "CX-55130771",
"createdByOperatorDescription": "Entrada Principal"
}
],
"nextCursor": null
}
Aqui createdByOperatorUsername/createdByOperatorDescription são sempre
os seus mesmos — esse endpoint é autoserviço.
Mesmos filtros/paginação do extrato de wallet (type, occurredAfter,
occurredBefore, sortDirection, limit, cursor), mais um a mais:
userId, pra filtrar só as operações feitas num participante específico.
Diferença importante: occurredAfter aqui tem um teto — a API nunca
mostra nada com mais de 48 horas, não importa o que você peça. Não é um
valor padrão que dá pra contornar; é um limite de verdade, imposto pelo
servidor. Existe porque não há conceito de turno de caixa ainda — sem um
"início de turno" formal, essa janela fixa evita que a sessão do caixa
consiga navegar um histórico enorme e desnecessário.
Esse endpoint é só de identificação — serve pra você mesmo conferir o que fez (ex.: bater com o caixa físico no fim do dia), não existe (ainda) nenhuma ação de estorno de recarga a partir daqui.
O que ainda não existe
- Verificação/consumo da credencial QR impressa pelo PDV (a emissão e o reset de PIN já existem; falta o PDV conseguir usar isso num pagamento).
- Turno de caixa (abrir com valor inicial em espécie, fechar com
conciliação) — a auditoria da recarga hoje usa direto o
operatorAccountIddo JWT, sem turno/caixa físico provisionado.