Skip to main content

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:

StatusDescriptionActions Allowed
BASKETOrder created, items being addedAdd/remove items, modify attendees, update buyer data
PRE_CONFIRMOrder ready for paymentPayment processing, can revert to BASKET if needed
CONFIRMOrder completed and paidFinal state - tickets issued, no modifications allowed
Important

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-id header 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_CONFIRM status
  • 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
Post-Purchase Operations

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.

Key Concept

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:

{
"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.

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 call POST /orders/item-attendees for this item
  • false or null β†’ Attendee data not required
Best Practice

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-selection
  • UNNUMBERED β†’ 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 via POST /orders/item-attendees
  • attendee.required === false or null β†’ Skip attendee step
tip

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 available
  • availability[].ticket_type β†’ NUMBERED or UNNUMBERED
  • availability[].attendee_settings β†’ Required attendee fields
  • availability[].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:

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

  1. Fetch venue map: GET /catalog-api/v1/sessions/{sessionId}/venue-map
  2. Fetch seat availability: GET /catalog-api/v1/sessions/{sessionId}/venue-map-availability
  3. Render interactive seat map with available/unavailable seats
  4. User selects seats
  5. 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.

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

When to Use

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: true fields
  • βœ… 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 code is 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.

Timeout

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.



πŸŽ“ Quick Reference​

When to Collect Attendee Data?​

CheckLocationIndicator
Before orderSession availabilityattendee_settings not null
After adding itemsOrder itemsitems[].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