Resume a purchase
The buyer's single source of truth for a purchase (issue #61: "fonte da verdade: tabela de purchases"): reads `purchases` + `purchase_items` and, when the purchase is still `pending` and carries a Payssage intent, the intent's current status — so a resume surfaces `intent.updated` without polling. Owner-only: the caller must be the purchase's `purchaser_id`; anything else is 404 (no existence leak). Strictly a read — the webhook receiver and the expiry worker own all status changes.
/checkouts/{purchase_id}The buyer's single source of truth for a purchase (issue #61: "fonte
da verdade: tabela de purchases"): reads purchases + purchase_items
and, when the purchase is still pending and carries a Payssage
intent, the intent's current status — so a resume surfaces
intent.updated without polling. Owner-only: the caller must be the
purchase's purchaser_id; anything else is 404 (no existence leak).
Strictly a read — the webhook receiver and the expiry worker own all
status changes.
Authorization
bearerAuth JWT issued by IdentityX, signed with Ed25519
In: header
Path Parameters
Purchase ID.
Response Body
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/checkouts/497f6eca-6276-4993-bfeb-53cbbbba6f08"{
"code": 0,
"message": "string",
"error": {
"code": "BAD_REQUEST",
"message": "string",
"fields": [
{
"field": "string",
"message": "string"
}
],
"meta": {},
"debug": {
"raw_error": "string",
"stack_trace": "string"
}
},
"pagination": {
"page": 1,
"limit": 1,
"total": 0,
"has_next": true,
"has_prev": true,
"next_page": 0,
"prev_page": 0
},
"timestamp": "2019-08-24T14:15:22Z",
"module": "string",
"data": {
"purchase_id": "06b23ec9-3221-4327-b592-c31d5c56b2f8",
"edition_id": "aeda7fca-56fe-426a-8c37-1876faaaf005",
"status": "pending",
"status_reason": "string",
"total_cents": 0,
"currency": "string",
"payment_method": "string",
"payssage_intent_id": "00469442-a68b-4aa4-a184-b15bbc9c37fe",
"qr_code": "string",
"qr_code_base64": "string",
"expires_at": "2019-08-24T14:15:22Z",
"created_at": "2019-08-24T14:15:22Z",
"items": [
{
"item_type": "ticket",
"item_id": "4d8cd62e-a579-4dae-af8c-3172f96f8f7c",
"quantity": 1,
"unit_price_cents": 0
}
],
"intent_status": "string"
}
}{
"code": 0,
"message": "string",
"error": {
"code": "BAD_REQUEST",
"message": "string",
"fields": [
{
"field": "string",
"message": "string"
}
],
"meta": {},
"debug": {
"raw_error": "string",
"stack_trace": "string"
}
},
"timestamp": "2019-08-24T14:15:22Z",
"module": "string"
}{
"code": 0,
"message": "string",
"error": {
"code": "BAD_REQUEST",
"message": "string",
"fields": [
{
"field": "string",
"message": "string"
}
],
"meta": {},
"debug": {
"raw_error": "string",
"stack_trace": "string"
}
},
"timestamp": "2019-08-24T14:15:22Z",
"module": "string"
}{
"code": 0,
"message": "string",
"error": {
"code": "BAD_REQUEST",
"message": "string",
"fields": [
{
"field": "string",
"message": "string"
}
],
"meta": {},
"debug": {
"raw_error": "string",
"stack_trace": "string"
}
},
"timestamp": "2019-08-24T14:15:22Z",
"module": "string"
}Check out an edition cart (reserve + create payment intent) POST
The money path (issue #61, split 7): reserves the requested items and creates the Payssage payment intent in one request. Prices are always server-computed from the DB (never trusted from the client). `purchase_items` is the availability ledger: the item rows are locked `FOR UPDATE` inside the checkout transaction before availability is checked, so two concurrent checkouts cannot oversell the last unit (the loser gets 409 listing the unavailable items). Flow: one transaction materializes the pending purchase (`purchases` + `purchase_items` + `registrations` / `product_purchases` / `program_participations`) and schedules the expiry job (`purchases.expire`, +10:01); the Payssage intent is created after commit; the intent id is stored back on the purchase in a second transaction. Only the Payssage webhook confirms payment (D3) — checkout never self-approves. Cards charge synchronously: the intent comes back `succeeded` but the purchase stays `pending` until the webhook confirms. Pix returns the QR (`qr_code` / `qr_code_base64`) in the response. Free orders (`total_cents == 0`) confirm immediately — no intent, no expiry job: the purchase is `approved` and materialized rows are created confirmed (badge emitted). Gifting: ticket lines are one-per-person (`quantity: 1`, one `attendee` each) — self-purchase sends the purchaser's own user_id/email/name; a gifted ticket sends the recipient's, resolved by the front from their email via IdentityX. `attendee.user_id` is trusted as-is. Buying N tickets for the same ticket type sends N lines. One active ticket per person per edition is enforced — the same attendee cannot appear twice in the cart (400), and any attendee who already holds a pending/confirmed registration for the edition is rejected (409); cancelled/expired registrations free the slot. Program lines require ≥1 ticket line in the same cart (a participation attaches to the ticket's registration); program quantity is fixed at 1. One active `pending` purchase per user+edition is enforced by a partial unique index — a second checkout returns 409 with the existing purchase's id (client-key idempotency is a follow-up).
The caller's current ticket for an edition GET
The authenticated user's active ticket for the edition — their own registration (`attendee_user_id` = caller) in `pending` (reserved, unpaid) or `confirmed` (paid) state — with its ticket type, so the front can show upgrade options on the more expensive ticket types. `data` is `null` when the caller holds no ticket (normal flow: they buy fresh). Read-only; never transitions state. The upgrade itself is a follow-up — this is only the "what do I hold" read.