Check out an edition cart (reserve + create payment intent)
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).
/editions/{edition_id}/checkoutThe 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 backsucceededbut the purchase stayspendinguntil 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).
Authorization
bearerAuth JWT issued by IdentityX, signed with Ed25519
In: header
Path Parameters
Edition ID.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
The cart + payment request (split 7). Prices are never trusted from
the client — the server recomputes them from the DB. For cards,
payment_method_id comes from the front's MercadoPago tokenization
(visa/master/...); for pix the server derives pix (no
tokenization). card_token is required for cards; installments
defaults to 1 when omitted.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/editions/497f6eca-6276-4993-bfeb-53cbbbba6f08/checkout" \ -H "Content-Type: application/json" \ -d '{ "payment_method": "pix", "payer": { "email": "buyer@example.com", "identification_type": "CPF", "identification_number": "12345678900" }, "items": [ { "item_type": "ticket", "item_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": 1, "attendee": { "user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "email": "buyer@example.com", "name": "Jane Doe" } }, { "item_type": "ticket", "item_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": 1, "attendee": { "user_id": "4fa85f64-5717-4562-b3fc-2c963f66afa6", "email": "friend@example.com", "name": "John Doe" } }, { "item_type": "product", "item_id": "5fa85f64-5717-4562-b3fc-2c963f66afa6", "quantity": 2 } ] }'{
"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
}
],
"ws_token": "string",
"ws_token_expires_at": "2019-08-24T14:15:22Z"
}
}{
"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"
}{
"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"
}Publish an edition POST
Publishes a draft edition, making it publicly visible and open for registrations. The actor must be an owner or admin of the event.
Resume a purchase GET
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.