Skip to main content

Pay via QR or NFC

The PDV declares an amount and generates a payload — a QR on screen or transmitted over NFC, either way, the app treats both the same. The participant's app reads that payload, confirms the payment from their own app (login is already the authentication factor — no PIN, unlike the cashier's printed QR flow, which has no login behind it at all).

PDV declares the amount
→ PDV generates the payload (QR/NFC) with an opaque token
→ participant's app reads the payload
→ app resolves the token on the server (shows amount + PDV before anything else)
→ participant confirms
→ app approves the payment (debits the wallet)

The payload doesn't contain the amount — just event and a single-use opaque token:

wallet://payment?v=1&event=<eventId>&intent=<token>

The app extracts eventId and token from this payload; the real amount only ever comes from the resolve step below — never trust anything other than the server's own response to decide how much will be charged.

1. Resolve (before confirming)​

EndpointWhat it does
GET /v1/events/{eventId}/wallet/payment-intents/{token}Read-only — never charges anything. Returns the amount and which PDV declared it, to render the confirmation screen.
curl "http://127.0.0.1:8787/v1/events/{eventId}/wallet/payment-intents/{token}" \
-H "Authorization: Bearer <participant accessToken>"
Response
{
"intentId": "018f26d7-8a7b-7e7d-8c96-4f0b8d0f2b13",
"amountMinor": 2500,
"status": "pending",
"operatorUsername": "PDV-00184723",
"operatorDescription": "Entrada Principal",
"expiresAt": "2026-09-09T12:05:00.000Z"
}

404 PAYMENT_INTENT_NOT_FOUND if the token doesn't exist (or expired a while ago and was cleaned up) in this event.

2. Approve (the participant's "Confirm" tap)​

EndpointWhat it does
POST /v1/events/{eventId}/wallet/payment-intents/{token}/approveDebits the authenticated participant's wallet by the intent's amount, credits the event's EVENT_REVENUE.

Requires the Idempotency-Key header — same mechanism already documented for the cashier's top-up: repeating the same key returns the same result without charging again (replayed: true in the response, meaning nothing was debited just now).

curl -X POST "http://127.0.0.1:8787/v1/events/{eventId}/wallet/payment-intents/{token}/approve" \
-H "Authorization: Bearer <participant accessToken>" \
-H "Idempotency-Key: <client-generated uuid>"
Response
{
"walletId": "018f26d7-...",
"eventId": "018f26d7-...",
"availableAmountMinor": 16750,
"replayed": false
}

What can go wrong, and why:

CodeSituation
404 PAYMENT_INTENT_NOT_FOUNDInvalid token, or one from a different event.
404 WALLET_NOT_FOUNDYou haven't activated a wallet in this event yet.
409 PAYMENT_INTENT_ALREADY_RESOLVEDSomeone already approved, declined, or the intent already expired. Each intent can only ever be resolved once.
409 PAYMENT_INTENT_EXPIREDToo much time (short, a few minutes) passed with nobody approving. Ask the PDV for a new QR.
409 INSUFFICIENT_BALANCEYour wallet doesn't have enough balance.
409 IDEMPOTENCY_KEY_IN_PROGRESSAn approval with this same key is already in flight; retry shortly.

Unlike the cashier's printed QR, there's no PIN here — the app's own login is already the factor proving who's paying. That's exactly why the explicit confirmation in step 2 is mandatory: the app never approves on its own, without the participant seeing the amount and tapping confirm.