Skip to main content

Cashier — overview

Cashier is an operational account (operator_type=cashier), created by management and scoped to a specific workspace — see Management → Operators (PDV and cashier).

Authentication

EndpointWhat it does
POST /v1/auth/cashier/loginLogin with the username/password generated by management. Sets wallet_cashier_access_token, wallet_cashier_refresh_token, wallet_cashier_csrf_token.
POST /v1/auth/cashier/refreshRotates the refresh token.
POST /v1/auth/cashier/logoutRevokes the session and clears the wallet_cashier_* cookies.

The cashier JWT follows the same shape as the PDV's — swap operator_type for "cashier". See PDV → overview for the full claims example.

Finding or registering a participant

Two global routes (wallet-control-api), with no eventId because identity is global — balance/wallet live in the region:

EndpointWhat it does
GET /v1/cashier/participants/by-phone/{phone}Read-only lookup, never creates anything. 404 PARTICIPANT_NOT_FOUND if the phone has no profile yet.
POST /v1/cashier/participantsFinds or creates by phone. If new, creates the profile without auth_user_id (they haven't installed the app or confirmed an OTP yet — that happens later, automatically, the first time they confirm an OTP for the same phone). Always returns an activationAuthorization: a signed, single-use authorization valid only for this participant and the cashier's event.
curl -X POST http://127.0.0.1:8787/v1/cashier/participants \
-H "Authorization: Bearer <cashier accessToken>" \
-H "Content-Type: application/json" \
-d '{ "phone": "+5511999999999" }'

Looking up and activating the wallet on the participant's behalf

Two regional routes (wallet-api, /v1/events/{eventId}/...):

EndpointWhat it does
GET /v1/events/{eventId}/cashier/wallets/{userId}Read-only lookup: this participant's wallet balance and status in this event. 404 WALLET_NOT_FOUND if they haven't activated yet.
POST /v1/events/{eventId}/cashier/wallets/activateCreates the wallet, using the activationAuthorization obtained in the previous step. Idempotent. The authorization can only be used once — even if activation fails for another reason, it's consumed; request a new one on error.
GET /v1/events/{eventId}/cashier/wallets/{userId}/statementPaginated statement of this wallet's movements (recharges today; sales/refunds once those exist).
curl -X POST http://127.0.0.1:8787/v1/events/{eventId}/cashier/wallets/activate \
-H "Authorization: Bearer <cashier accessToken>" \
-H "Content-Type: application/json" \
-d '{ "userId": "<userId returned in the previous step>", "activationAuthorization": "<token returned in the previous step>" }'

Statement

curl "http://127.0.0.1:8787/v1/events/{eventId}/cashier/wallets/{userId}/statement?limit=20&sortDirection=desc" \
-H "Authorization: Bearer <cashier accessToken>"
Response
{
"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 identify who performed the movement (the cashier, or the PDV in the future) — they're snapshotted onto the ledger entry at the moment it happens, so they keep showing who the operator was at the time even if the account gets renamed later. Both are null when there's no operator, or when the account has no description set.

Cursor-paginated (not numbered pages): when nextCursor comes back non-null, call again with ?cursor=<value> for the next batch — keep the same sortDirection/filters as the original call, or the API rejects with WALLET_STATEMENT_CURSOR_SORT_MISMATCH.

Available filters, all optional and combinable:

ParameterWhat it does
typeOnly one movement type (today only recharge exists).
occurredAfter / occurredBeforeDate range, ISO 8601, inclusive on both ends.
sortDirectionasc or desc (default: desc, newest first).
limit1 to 100 (default 50).

amountMinor is signed — positive is a credit (money coming in), negative is a debit. paymentMethod/note only appear on recharge entries; they'll be null for other movement types in the future.

Top-up

EndpointWhat it does
POST /v1/events/{eventId}/cashier/topupsRecharges the participant's wallet. Always immediate — there's no external payment confirmation in this phase, the cashier just declares how the money arrived.

paymentMethod is free text, not a fixed list — "cash", "credit_card", "pix", or whatever category the organization uses. Requires the Idempotency-Key header: repeating the same key returns the same result, without crediting twice — even if both calls arrive at the same time, they never run the recharge in parallel. If a second call with the same key arrives while the first is still in flight, it's rejected with 409 IDEMPOTENCY_KEY_IN_PROGRESS (rather than waiting) — retry shortly, it's not a permanent error.

curl -X POST http://127.0.0.1:8787/v1/events/{eventId}/cashier/topups \
-H "Authorization: Bearer <cashier accessToken>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <client-generated uuid>" \
-d '{ "userId": "<participant userId>", "amountMinor": 5000, "paymentMethod": "cash" }'

Printed QR credential with a PIN

For a participant without a phone/app:

EndpointWhat it does
POST /v1/events/{eventId}/cashier/wallet-credentials/printed-qrIssues a revocable credential linked to the wallet. The system generates a random PIN and returns it in the response body exactly once — it's never stored in plaintext anywhere, only its hash. Reissuing automatically revokes the previous credential — a wallet never has more than one active at a time.

There's no standalone PIN-reset endpoint that keeps the same QR. QR and PIN are two independent factors — the PDV reads the QR to find the credential, then asks for the PIN to validate that specific credential. One leaking alone is useless; only both together are a problem. So whenever there's any suspicion (PIN seen, card lost, whatever), the answer is always to reissue everything — there's no way to be sure only one of the two leaked. Even a plain "participant forgot their PIN," with no suspicion at all, also goes through reissuing (it's the only option available today).

The printed QR cannot be used to pay yet — the PDV doesn't have the read/consume side of this credential implemented yet.

My own operations

GET /v1/events/{eventId}/cashier/operations — the mirror image of the statement above: instead of every movement on one wallet, it's every operation you performed, across however many wallets you touched. Always self-service — there's no parameter to see another cashier's activity, only your own, identified from your own token.

curl "http://127.0.0.1:8787/v1/events/{eventId}/cashier/operations?limit=20" \
-H "Authorization: Bearer <cashier accessToken>"
Response
{
"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
}

Here createdByOperatorUsername/createdByOperatorDescription are always your own — this endpoint is self-service.

Same filters/pagination as the wallet statement (type, occurredAfter, occurredBefore, sortDirection, limit, cursor), plus one more: userId, to filter down to operations performed on one specific participant.

Important difference: occurredAfter has a ceiling here — the API never shows anything older than 48 hours, no matter what you ask for. It's not a default you can work around; it's a real limit, enforced by the server. It exists because there's no cashier shift concept yet — with no formal "shift start" to anchor against, this fixed window keeps a cashier session from browsing a huge, unnecessary amount of history.

This endpoint is for identification only — it's meant for you to check what you did (e.g. reconciling against the physical cash drawer at the end of the day), there's no recharge reversal action available from here (yet).

What doesn't exist yet

  • PDV-side verification/consumption of the printed QR credential (issuance and PIN reset already exist; the PDV still can't use it in a payment).
  • Cashier shift (opening with an initial cash amount, closing with reconciliation) — today, recharge auditing uses the JWT's operatorAccountId directly, with no shift/physical cash register provisioned.