PDV — Payment via printed QR + PIN
Regional route (wallet-api, /v1/events/{eventId}/...), for a participant
without the app: the cashier already issued the credential (see Cashier →
printed QR credential with a PIN);
the PDV scans the QR, enters the amount, and asks the participant to type
their PIN.
| Endpoint | What it does |
|---|---|
POST /v1/events/{eventId}/pdv/payments | Debits the participant's wallet by the given amount and credits the event's EVENT_REVENUE account, all in the same transaction. |
- curl
- JavaScript
- Python
curl -X POST http://127.0.0.1:8787/v1/events/{eventId}/pdv/payments \
-H "Authorization: Bearer <PDV accessToken>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <client-generated uuid>" \
-d '{
"qrPayload": "wallet://credential?v=1&event=<eventId>&credential=<QR token>",
"pin": "482913",
"amountMinor": 2500
}'
const response = await fetch(`http://127.0.0.1:8787/v1/events/${eventId}/pdv/payments`, {
method: "POST",
headers: {
Authorization: `Bearer ${pdvAccessToken}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ qrPayload, pin, amountMinor: 2500 }),
});
const { walletId, availableAmountMinor } = await response.json();
import requests
import uuid
response = requests.post(
f"http://127.0.0.1:8787/v1/events/{event_id}/pdv/payments",
headers={
"Authorization": f"Bearer {pdv_access_token}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"qrPayload": qr_payload, "pin": pin, "amountMinor": 2500},
)
result = response.json()
{
"walletId": "018f26d7-...",
"eventId": "018f26d7-...",
"availableAmountMinor": 16750,
"replayed": false
}
What the endpoint checks, in this order:
- Is the QR from this event?
qrPayloadcarries its owneventIdinside it — if it doesn't match the URL's event, the response is404 CREDENTIAL_NOT_FOUND, same as an unknown QR (it never reveals that the token exists, only that it isn't valid here). - Is the credential active, unexpired, and unlocked? If not,
409 CREDENTIAL_NOT_ACTIVE/409 CREDENTIAL_EXPIRED/403 CREDENTIAL_LOCKED. - Does the PIN match? If not,
403 INVALID_PIN— and the attempt is recorded against the credential. 5 wrong attempts in a row lock the credential for 15 minutes (403 CREDENTIAL_LOCKED); the participant has to wait, or the cashier has to reissue a new credential. - Is there enough balance? If not,
409 INSUFFICIENT_BALANCE.
The wallet stays locked for the entire check-and-debit — if two PDVs try to charge the same wallet at the same time (or a recharge and a charge happen to overlap), one waits for the other to finish; they never run in parallel.
Requires the Idempotency-Key header, with the same behavior as the
cashier's top-up: repeating the same key returns the same
result without charging twice (replayed: true in the response, to make it
clear nothing was charged again), and a second call with the same key while
the first is still in flight gets 409 IDEMPOTENCY_KEY_IN_PROGRESS.
Reusing the same key with a different amount or QR gets 409 IDEMPOTENCY_KEY_BODY_MISMATCH instead of charging (or returning) the wrong
value — generate a new key for each charge.