Complete Purchase Flow Guide
This guide walks you through the complete ticket purchase flow from order creation to confirmation.
π Main Conceptsβ
Orderβ
An order is a JSON object that represents a complete Onebox transaction. It contains all the information about tickets, buyer, attendees, pricing, and payment.
Order Lifecycleβ
Every order goes through different statuses from creation to completion:
| Status | Description | Actions Allowed |
|---|---|---|
| BASKET | Order created, items being added | Add/remove items, modify attendees, update buyer data |
| PRE_CONFIRM | Order ready for payment | Payment processing, can revert to BASKET if needed |
| CONFIRM | Order completed and paid | Final state - tickets issued, no modifications allowed |
Once an order reaches CONFIRM status, it cannot be modified. All changes must be done via refunds or cancellations through the Orders Management API.
Order Identifiersβ
An order has two different identifiers depending on its status:
id - Internal Order ID (Pre-Order)β
- When: Created immediately when order is created (BASKET status)
- Format: Hexadecimal hash (e.g.,
52bf39e0dbc283812f99029494db0346) - Usage: Must be sent in the
ob-order-idheader for all order modification calls - Scope: Used during order building phase (BASKET β PRE_CONFIRM)
# Example using pre-order id
POST /distribution-api/v1/orders/seats
Header: ob-order-id: 52bf39e0dbc283812f99029494db0346
code - Order Codeβ
- When: Generated when order moves to
PRE_CONFIRMstatus - Format: Alphanumeric code (e.g.,
AX59SJ29392X) - Usage: Unique identifier for confirmed orders
- Scope: Used for order queries, refunds, customer service
# Example using order code
GET /orders-mgmt-api/v1/orders/AX59SJ29392X
After confirmation, use the order code with the Orders Management API to:
- Retrieve order details
- Process refunds
- Check ticket status
- Download tickets
π« Understanding Attendee Requirementsβ
What are Attendees?β
Attendees are the people who will actually use the tickets. They may be different from the buyer (the person paying).
When are Attendees Required?β
Attendee data collection is configured by the event promoter, NOT by Onebox. Whether you need to collect attendee information depends entirely on how the promoter set up the event.
Attendee requirements are event-specific. Some events require full attendee details (name, ID, email), while others don't require any attendee information at all.
How to Check if Attendees are Requiredβ
You can determine attendee requirements in two ways:
Option 1: Check Session Availability (Before Creating Order)β
GET /catalog-api/v1/sessions/{sessionId}/availability
Response example:
- Attendees Required
- No Attendees Required
{
"session_id": 2633049,
"availability": [
{
"price_type_id": 123,
"price": 50.00,
"available_tickets": 100,
"attendee_settings": {
"ATTENDANT_NAME": {
"required": true,
"validation": "^[a-zA-Z ]+$"
},
"ATTENDANT_SURNAME": {
"required": true
},
"ATTENDANT_EMAIL": {
"required": true,
"validation": "^[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"
}
}
}
]
}
β This event requires name, surname, and email for each ticket holder.
{
"session_id": 2633050,
"availability": [
{
"price_type_id": 124,
"price": 30.00,
"available_tickets": 200,
"attendee_settings": null
}
]
}
β This event does NOT require any attendee information. Only buyer data is needed.
Option 2: Check Order Items (After Adding Items)β
After adding items to your order, check each item in the order response:
GET /distribution-api/v1/orders/{orderId}
Response example:
{
"id": "52bf39e0dbc283812f99029494db0346",
"status": {
"type": "BASKET"
},
"items": [
{
"item_id": 1,
"session_id": 2633049,
"price": 50.00,
"attendee": {
"required": true,
"fields": {
"ATTENDANT_NAME": {
"required": true,
"validation": "^[a-zA-Z ]+$"
},
"ATTENDANT_SURNAME": {
"required": true
},
"ATTENDANT_EMAIL": {
"required": true
}
}
}
}
]
}
Key field: items[].attendee.required
trueβ You must callPOST /orders/item-attendeesfor this itemfalseornullβ Attendee data not required
Always check attendee_settings or items[].attendee to determine if attendee data collection is necessary. Never assume all events require (or don't require) attendees.
π Complete Purchase Flowβ
Flow Overviewβ
Decision Pointsβ
1οΈβ£ Numbered vs Unnumbered?β
Check ticket_type in availability response:
NUMBEREDβ User selects specific seats OR use auto-selectionUNNUMBEREDβ Only quantity selection (use/seats-auto)
2οΈβ£ Attendee Data Required?β
Check items[].attendee.required in the order response after adding seats:
attendee.required === trueβ Collect attendee information viaPOST /orders/item-attendeesattendee.required === falseornullβ Skip attendee step
You can preview attendee requirements in the session availability response (attendee_settings), but the definitive check is the item descriptor after seats are added.
π Step-by-Step Implementationβ
Step 1: Get Session Availabilityβ
Before creating an order, retrieve availability to understand:
- β Available tickets and pricing
- β Whether seats are numbered or unnumbered
- β Required attendee information
- β Event-specific rules
GET /catalog-api/v1/sessions/{sessionId}/availability
What to look for:
availability[].available_ticketsβ How many tickets availableavailability[].ticket_typeβNUMBEREDorUNNUMBEREDavailability[].attendee_settingsβ Required attendee fieldsavailability[].priceβ Ticket price
Step 2: Create Orderβ
Create an empty order to start the purchase process:
POST /distribution-api/v1/orders
curl --location --request POST 'https://{{api-domain}}/distribution-api/v1/orders' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer {{access_token}}'
Response:
{
"id": "52bf39e0dbc283812f99029494db0346",
"status": {
"type": "BASKET"
},
"items": [],
"created_at": "2026-02-12T10:00:00Z"
}
Save the id - you'll need it for all subsequent calls in the ob-order-id header.
Step 3: Add Items to Orderβ
You have two options depending on event type:
- Numbered - Manual Selection
- Auto Selection (Numbered or Unnumbered)
Use when: Event has numbered seats AND user wants to pick specific seats.
POST /distribution-api/v1/orders/seats
curl --location --request POST 'https://{{api-domain}}/distribution-api/v1/orders/seats' \
--header 'Accept: application/json' \
--header 'ob-order-id: 52bf39e0dbc283812f99029494db0346' \
--header 'Authorization: Bearer {{access_token}}' \
--header 'Content-Type: application/json' \
--data-raw '{
"seats": [
{
"id": 45678,
"session_id": 2633049
},
{
"id": 45679,
"session_id": 2633049
}
]
}'
How to get seat IDs:
You have two options:
Option A: Build your own seat map
- Fetch venue map:
GET /catalog-api/v1/sessions/{sessionId}/venue-map - Fetch seat availability:
GET /catalog-api/v1/sessions/{sessionId}/venue-map-availability - Render interactive seat map with available/unavailable seats
- User selects seats
- Send selected seat IDs to
POST /orders/seats
Option B: Use Onebox Widget Integrate our pre-built seat selection widget that handles the entire flow for you, including promotion handling and visual seat selection, etc. See Widget SDK documentation for implementation details.
Use when:
- Event is unnumbered (general admission)
- Event is numbered but you want best available seats
POST /distribution-api/v1/orders/seats-auto
curl --location --request POST 'https://{{api-domain}}/distribution-api/v1/orders/seats-auto' \
--header 'Accept: application/json' \
--header 'ob-order-id: 52bf39e0dbc283812f99029494db0346' \
--header 'Authorization: Bearer {{access_token}}' \
--header 'Content-Type: application/json' \
--data-raw '{
"session_id": 2633049,
"price_type_id": 123,
"rate_id": 789,
"quantity": 2
}'
System automatically:
- Selects best available seats (for numbered)
- Reserves tickets (for unnumbered)
- Ensures seats are together when possible
zone_idandprice_type_id: HIGHLY RECOMMENDED to ensure correct price selectionrate_id: Optional, but recommended for accurate pricing- Without these, the system may select unexpected prices or zones
Response (both methods):
{
"id": "52bf39e0dbc283812f99029494db0346",
"status": {
"type": "BASKET"
},
"items": [
{
"item_id": 1,
"session_id": 2633049,
"price_type_id": 123,
"price": 50.00,
"seat": {
"section": "101",
"row": "A",
"seat_number": "15"
},
"attendee": {
"required": true,
"fields": {
"ATTENDANT_NAME": { "required": true },
"ATTENDANT_SURNAME": { "required": true },
"ATTENDANT_EMAIL": { "required": true }
}
}
},
{
"item_id": 2,
"session_id": 2633049,
"price_type_id": 123,
"price": 50.00,
"seat": {
"section": "101",
"row": "A",
"seat_number": "16"
},
"attendee": {
"required": true,
"fields": {
"ATTENDANT_NAME": { "required": true },
"ATTENDANT_SURNAME": { "required": true },
"ATTENDANT_EMAIL": { "required": true }
}
}
}
],
"total": 100.00
}
Check: items[].attendee.required to see if next step is needed.
Step 4: Add Attendee Data (If Required)β
Only call this endpoint if items[].attendee.required is true.
If attendee.required is false or null, skip this step and go directly to Step 5.
POST /distribution-api/v1/orders/item-attendees
curl --location --request POST 'https://{{api-domain}}/distribution-api/v1/orders/item-attendees' \
--header 'Accept: application/json' \
--header 'ob-order-id: 52bf39e0dbc283812f99029494db0346' \
--header 'Authorization: Bearer {{access_token}}' \
--header 'Content-Type: application/json' \
--data-raw '[
{
"item_id": 1,
"field": {
"ATTENDANT_NAME": "John",
"ATTENDANT_SURNAME": "Smith",
"ATTENDANT_EMAIL": "john.smith@example.com"
}
},
{
"item_id": 2,
"field": {
"ATTENDANT_NAME": "Mary",
"ATTENDANT_SURNAME": "Johnson",
"ATTENDANT_EMAIL": "mary.johnson@example.com"
}
}
]'
Important:
- β
Send one object per
item_id(one per ticket) - β
Include all
required: truefields - β Match validation patterns if provided
- β Don't send empty strings for required fields (use valid value or omit)
Step 5: Add Buyer Dataβ
The buyer is the person purchasing the tickets (paying customer):
POST /distribution-api/v1/orders/buyer-data
curl --location --request POST 'https://{{api-domain}}/distribution-api/v1/orders/buyer-data' \
--header 'Accept: application/json' \
--header 'ob-order-id: 52bf39e0dbc283812f99029494db0346' \
--header 'Authorization: Bearer {{access_token}}' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Jane",
"surname": "Doe",
"email": "jane.doe@example.com",
"phone": "+34611222333",
"country": "ES",
"postal_code": "28001"
}'
Required fields:
email- Always required for order confirmation and tickets- Other fields depend on your channel configuration
Response:
{
"id": "52bf39e0dbc283812f99029494db0346",
"status": {
"type": "BASKET"
},
"buyer": {
"name": "Jane",
"surname": "Doe",
"email": "jane.doe@example.com",
"phone": "+34611222333"
},
"items": [...],
"total": 100.00
}
Step 6: Pre-Confirm Orderβ
Lock the order and prepare for payment:
POST /distribution-api/v1/orders/pre-confirm
curl --location --request POST 'https://{{api-domain}}/distribution-api/v1/orders/pre-confirm' \
--header 'Accept: application/json' \
--header 'ob-order-id: 52bf39e0dbc283812f99029494db0346' \
--header 'Authorization: Bearer {{access_token}}' \
--header 'Content-Type: application/json' \
--data-raw '{}'
What happens:
- β
Order status changes to
PRE_CONFIRM - β
Order
codeis generated (e.g.,ORD-ABC123XYZ) - β Seats/tickets are reserved (temporary hold)
- β Order summary is finalized
Response:
{
"id": "52bf39e0dbc283812f99029494db0346",
"code": "AX59SJ29392X",
"status": {
"type": "PRE_CONFIRM"
},
"buyer": {...},
"items": [...],
"total": 100.00,
"currency": "EUR",
"expires_at": "2026-02-12T10:15:00Z"
}
Important: The order now has an expiration time. Complete payment before expiration or items will be released.
Pre-confirmed orders typically expire after 20 minutes. Complete payment quickly to avoid losing reserved tickets.
Step 7: Process Paymentβ
Process payment using your payment provider (Stripe, PayPal, Redsys, etc.).
This step happens outside Onebox APIs - you handle payment with your own payment gateway.
// Example with Stripe
const paymentIntent = await stripe.paymentIntents.create({
amount: 10000, // 100.00 EUR in cents
currency: 'eur',
metadata: {
onebox_order_id: '52bf39e0dbc283812f99029494db0346',
onebox_order_code: 'AX59SJ29392X'
}
});
// Confirm payment with customer
const result = await stripe.confirmCardPayment(
paymentIntent.client_secret,
{ payment_method: paymentMethodId }
);
if (result.error) {
// Payment failed - handle error
// Order will expire and tickets released
} else {
// Payment successful - confirm order
await confirmOneboxOrder('52bf39e0dbc283812f99029494db0346');
}
Step 8: Confirm Orderβ
After successful payment, finalize the order:
POST /distribution-api/v1/orders/confirm
curl --location --request POST 'https://{{api-domain}}/distribution-api/v1/orders/confirm' \
--header 'Accept: application/json' \
--header 'ob-order-id: 52bf39e0dbc283812f99029494db0346' \
--header 'Authorization: Bearer {{access_token}}' \
--header 'Content-Type: application/json' \
--data-raw '{}'
What happens:
- β
Order status changes to
CONFIRM - β Tickets are issued and become valid
- β Barcodes are generated
- β Order is final (no more modifications)
Response:
{
"id": "52bf39e0dbc283812f99029494db0346",
"code": "AX59SJ29392X",
"status": {
"type": "CONFIRM"
},
"buyer": {...},
"items": [
{
"item_id": 1,
"ticket": {
"barcode": "123456789012",
"barcode_url": "https://tickets.onebox.com/ticket/123456789012",
"barcode_pdf_url": "https://tickets.onebox.com/ticket/123456789012.pdf"
},
"seat": {...},
"attendee": {...}
}
],
"total": 100.00,
"confirmed_at": "2026-02-12T10:12:00Z"
}
Next steps:
- Send confirmation email to buyer
- Provide ticket download links
- Display order confirmation page
π― Common Scenariosβ
Scenario 1: Simple Event (No Attendees, Unnumbered)β
Example: Music festival, general admission
1. GET /sessions/{id}/availability
β attendee_settings: null
β ticket_type: UNNUMBERED
2. POST /orders
β id: 123456
3. POST /orders/seats-auto
β quantity: 2
4. POST /orders/buyer-data
β email, name, etc.
5. POST /orders/pre-confirm
6. [Process payment]
7. POST /orders/confirm
No attendee step needed!
Scenario 2: Event with Attendees (Numbered Seats)β
Example: Theater show, specific seats, attendee names required
1. GET /sessions/{id}/availability
β attendee_settings: { ATTENDANT_NAME, ATTENDANT_EMAIL }
β ticket_type: NUMBERED
2. POST /orders
3. POST /orders/seats (manual seat selection)
OR POST /orders/seats-auto
4. POST /orders/item-attendees β Required!
β Send name + email for each ticket
5. POST /orders/buyer-data
6. POST /orders/pre-confirm
7. [Process payment]
8. POST /orders/confirm
Scenario 3: Mixed Requirementsβ
Example: Event with both VIP (attendee required) and General (no attendee)
// After adding items
{
"items": [
{
"item_id": 1,
"price_type": "VIP",
"attendee": {
"required": true β Need attendee data
}
},
{
"item_id": 2,
"price_type": "General",
"attendee": {
"required": false β No attendee needed
}
}
]
}
Solution: Only send attendee data for item 1:
POST /orders/item-attendees
[
{
"item_id": 1,
"field": { "ATTENDANT_NAME": "John", ... }
}
// Don't include item_id: 2
]
β οΈ Common Errorsβ
Error: Missing Attendee Dataβ
{
"code": "ATTENDEE_DATA_REQUIRED",
"message": "Attendee data is required for item 1"
}
Cause: You called /pre-confirm without providing required attendee data.
Solution: Call POST /orders/item-attendees before pre-confirming.
Error: Invalid Attendee Fieldβ
{
"code": "ATTENDEE_ATTRIBUTE_INVALID",
"message": "ATTENDANT_EMAIL does not match validation pattern"
}
Cause: Attendee data doesn't match the validation regex.
Solution: Check attendee_settings[field].validation and ensure your value matches.
Error: Order Expiredβ
{
"code": "ORDER_EXPIRED",
"message": "Order has expired"
}
Cause: Too much time passed between pre-confirm and confirm.
Solution: Start a new order. Implement timeout warnings in your UI.
π Related Guidesβ
- Error Codes Reference - Complete error documentation
π Quick Referenceβ
When to Collect Attendee Data?β
| Check | Location | Indicator |
|---|---|---|
| Before order | Session availability | attendee_settings not null |
| After adding items | Order items | items[].attendee.required === true |
| Rule | - | If either shows requirements β collect data |
Endpoint Decision Treeβ
Need to add tickets?
ββ Numbered seats?
β ββ User picks seats? β POST /orders/seats
β ββ Auto-select? β POST /orders/seats-auto
ββ Unnumbered? β POST /orders/seats-auto
Attendee data required?
ββ Yes β POST /orders/item-attendees
ββ No β Skip, go to buyer data
Always required:
ββ POST /orders/buyer-data