TrieOH
UniventsAPICheckouts

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).

POST/editions/{edition_id}/checkout

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).

Authorization

bearerAuth
AuthorizationBearer <token>

JWT issued by IdentityX, signed with Ed25519

In: header

Path Parameters

edition_id*string

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"
}