Voiding a ticket
POST /api/partner/tickets/{bookingRef}/void — a refund and a cancellation are opposite obligations.
POST /api/partner/tickets/{bookingRef}/void
Authorization: Bearer <key>
Content-Type: application/json{
"reason": "CANCELLATION",
"message": "19:30 үзвэр цуцлагдлаа — Танхим 5"
}| Field | ||
|---|---|---|
reason | required | REFUND or CANCELLATION |
message | optional | Max 2000 chars. What the holder is told. Omit for silence |
{ "serialNumber": "8f14e45f-...", "state": "VOID", "reason": "CANCELLATION" }Both reasons land the pass at VOID and grey the card out — Apple's voided, Google's INACTIVE
object state. Both wallets sync on their own.
Why the reason is stored
A refund and a cancellation are one state change and opposite obligations.
REFUND— she asked for her money back. She knows the seat is gone; saying so is noise. In the merchant's numbers this is churn.CANCELLATION— the projector broke. She does not know, and telling her is the entire point of putting the ticket in a wallet in the first place. In the numbers this is breakage.
They are counted separately because a merchant reading a report needs them apart. reason is a
fixed pair, not free text, so it stays countable.
The message
Supply message and one notice goes out, written into the card so it reaches the lock screen.
Omit it and the card greys out in silence.
The message is operational: it checks neither the three-notices-per-customer-per-day cap nor marketing consent. A ticket buyer never consented to a mailing list, and this is not marketing — she paid for the seat and is about to turn up for it. There is no merchant-facing flag for this and there never will be one; it is derived from the program type and the notice's origin, because a flag every merchant ticks is not a cap.
The program's notification tokens render, so a message may carry the facts rather than repeat them:
{ "reason": "CANCELLATION", "message": "{{starts_at}} үзвэр цуцлагдлаа" }{{starts_at}} renders as MM-dd HH:mm in Asia/Ulaanbaatar. {{seat}} and {{hall}} also work
and render an em dash when unset.
Google's own quota is real and can still refuse an exempt notice. A half-landed cancellation is accepted knowingly as better than a suppressed one.
Idempotency
Voiding twice is a no-op: the second call returns 200 with the same body and the reason already stored, and sends no second notice. A retried cancellation must not put the same sentence on her lock screen twice.
This means the reason is fixed by the first successful call. Voiding a REFUND and then calling
again with CANCELLATION leaves it a REFUND and tells nobody.
Refusals
| Status | ||
|---|---|---|
Unknown bookingRef (or another merchant's) | 404 | no ticket with this bookingRef |
reason missing or not one of the two | 400 | field-bound |
| Missing or unknown key | 401 |
What voiding does not do
There is no un-void. There is no endpoint that moves a screening's time or hall, and none that re-seats a pass — if a screening moves, the tickets for it have to be voided and reissued against the new time. A screening-change call is a known gap rather than a decision, and lives in the backlog as the next thing this surface wants.