Checkout
The Checkout API enables cart-based ticket purchases with Stripe integration. The flow consists of two steps: initialize the checkout to create a cart and get Stripe credentials, then confirm the purchase after payment.
These endpoints authenticate with your sk_live_ secret key and are server-side only. To drive checkout from the browser instead, use the public checkout API with your publishable key. See which key, which API.
Checkout flow overview
- Initialize - Create a cart with selected tickets, receive Stripe PaymentIntent
- Apply a discount - Optional, and only before the card is confirmed. Reprices the PaymentIntent so the card is charged the discounted amount
- Collect payment - Use Stripe.js to collect payment details
- Confirm - Submit payment confirmation and attendee information
- Receive tickets - Get order confirmation with generated tickets
Responses put a single resource directly on data. Only list endpoints wrap their contents, and they use items.
Carts expire after 15 minutes by default, but organizers can configure this per event, and high-demand events shorten it automatically (as low as 5 minutes). Always read expires_at from the initialize response rather than assuming a fixed window. Once a cart expires you must initialize a new checkout.
This endpoint is for ticketed events only (both paid and free tickets). RSVP events use the /rsvp endpoint instead.
Initialize checkout
Initialize a checkout session by selecting a ticket type and quantity. Creates a cart with inventory reservation using pessimistic locking. For paid events, a Stripe PaymentIntent is created and credentials are returned.
Required attributes
- Name
ticket_type_id- Type
- string
- Description
ULID of the ticket type to purchase. Must be active and currently on sale.
- Name
quantity- Type
- integer
- Description
Number of tickets (must be within the ticket type's min/max per order limits and not exceed remaining inventory).
Optional attributes
- Name
occurrence_id- Type
- string
- Description
ULID of the specific occurrence date. Required for recurring events, ignored for single events.
Response attributes
- Name
cart_id- Type
- string
- Description
ULID of the created cart. Required for the confirm step.
- Name
stripe- Type
- object | null
- Description
Stripe credentials for payment collection. Null for free tickets.
- Name
stripe.publishable_key- Type
- string
- Description
Stripe publishable key for the event organizer's account.
- Name
stripe.client_secret- Type
- string
- Description
PaymentIntent client secret for Stripe.js.
- Name
stripe.connected_account_id- Type
- string | null
- Description
Non-null when the organizer is on Stripe Standard, which takes direct charges. Pass it to Stripe.js as
{ stripeAccount: connected_account_id }, or confirming the payment fails with a 404 because the PaymentIntent lives on the organizer's account rather than the platform's. Null for destination-charge organizers.
- Name
amount- Type
- integer
- Description
Total amount in cents (e.g. 2500 = $25.00). Zero for free tickets.
- Name
currency- Type
- string
- Description
ISO 4217 currency code (e.g. USD, GBP, CAD).
- Name
is_free_event- Type
- boolean
- Description
Whether this checkout has a zero total (free ticket purchase).
- Name
custom_questions- Type
- CustomQuestion[]
- Description
Array of custom questions to display during the details step. May be empty.
- Name
phone_field_type- Type
- string
- Description
required,optionaloroff, from the event's registration settings. Decides whetherbilling_phoneis mandatory when you confirm, so render the field accordingly.
- Name
expires_at- Type
- string
- Description
ISO 8601 datetime when the cart expires. After this time, the cart_id is invalid.
Validation rules
The following validation is performed before the cart is created:
- Ticket type: Must exist, belong to this event, be active, and be within its sale window (
available_from/available_until). - Quantity: Must be ≥
min_per_orderand ≤max_per_orderfor the ticket type, and not exceed remaining inventory. - Occurrence: For recurring events, must be a scheduled occurrence with a future date. Per-occurrence capacity is enforced separately from ticket type inventory.
All validation errors return HTTP 422.
Request
curl -X POST https://api.qariyb.com/api/v1/events/01HWX9KPQR/checkout/initialize \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"ticket_type_id": "01HWX9TICKET123",
"quantity": 2
}'
Response (Paid Event)
{
"success": true,
"code": 100,
"locale": "en",
"message": "OK",
"data": {
"cart_id": "01HWX9CART1234567890ABCD",
"stripe": {
"publishable_key": "pk_live_...",
"client_secret": "pi_3ABC123_secret_XYZ789",
"connected_account_id": null
},
"amount": 5000,
"currency": "USD",
"is_free_event": false,
"custom_questions": [
{
"id": "01HWX9Q123",
"question": "Dietary restrictions?",
"type": "text",
"required": true,
"options": [],
"display_order": 1
},
{
"id": "01HWX9Q456",
"question": "How did you hear about us?",
"type": "select",
"required": false,
"options": [
"Social Media",
"Friend",
"Masjid",
"Other"
],
"display_order": 2
}
],
"phone_field_type": "optional",
"expires_at": "2025-01-24T12:45:00+00:00"
}
}
Response (Free Ticket)
{
"success": true,
"code": 100,
"locale": "en",
"message": "OK",
"data": {
"cart_id": "01HWX9CART1234567890ABCD",
"stripe": null,
"amount": 0,
"currency": "USD",
"is_free_event": true,
"custom_questions": [],
"phone_field_type": "optional",
"expires_at": "2025-01-24T12:45:00+00:00"
}
}
Response (Recurring Event)
{
"success": true,
"code": 100,
"locale": "en",
"message": "OK",
"data": {
"cart_id": "01HWX9CART9876543210ZYXW",
"stripe": {
"publishable_key": "pk_live_...",
"client_secret": "pi_9DEF456_secret_ABC123",
"connected_account_id": null
},
"amount": 1500,
"currency": "USD",
"is_free_event": false,
"custom_questions": [],
"phone_field_type": "required",
"expires_at": "2025-01-24T12:45:00+00:00"
}
}
Error (Sold Out)
{
"success": false,
"code": 405,
"locale": "en",
"message": "Validation failed",
"data": null
}
Error (RSVP Event)
{
"success": false,
"code": 200,
"locale": "en",
"message": "This event uses RSVP registration. Use the /api/v1/events/{ulid}/rsvp endpoint instead.",
"data": null
}
Apply a discount
Validates a discount code against the cart and reprices the PaymentIntent, so the card is charged the reduced amount.
Call this after initialize and before confirming the card with Stripe. Stripe charges whatever the intent says at confirmation time, so a code applied afterwards would leave the buyer charged in full.
Confirm takes no discount code of its own: it reads the discount back off the PaymentIntent this call repriced, and records it on the order.
Applying a second code recomputes from the cart subtotal rather than stacking. There is no "remove" — the intent would stay repriced while confirm checked it against the full amount — so apply a different code instead.
Required attributes
- Name
cart_id- Type
- string
- Description
The cart ULID from the initialize response. Must be active and not expired.
- Name
code- Type
- string
- Description
The discount code (max 50 characters). Matched case-insensitively; the response echoes the canonical casing.
- Name
payment_intent_id- Type
- string
- Description
The Stripe PaymentIntent to reprice (starts with
pi_).
Response attributes
- Name
code- Type
- string
- Description
The code as stored, which may differ in case from what was sent.
- Name
discount_type- Type
- integer
- Description
1for a fixed amount,2for a percentage.
- Name
discount_type_name- Type
- string
- Description
Readable form:
fixed_amountorpercentage.
- Name
discount_value- Type
- integer
- Description
Whole percentage points for a percentage, minor units for a fixed amount.
- Name
formatted_value- Type
- string
- Description
Display-ready value, e.g.
10%or£5.00.
- Name
discount_amount- Type
- integer
- Description
Amount taken off, in minor units.
- Name
new_total- Type
- integer
- Description
What the card will now be charged, in minor units.
- Name
currency- Type
- string
- Description
Uppercase ISO 4217 currency code.
Errors
| HTTP | ApiCode | Scenario |
|---|---|---|
| 404 | 500 EVENT_NOT_AVAILABLE | Event ULID not found, not published, or another organization's. |
| 404 | 506 CART_NOT_FOUND | cart_id is not a cart for this event. |
| 410 | 503 CART_EXPIRED | Cart is no longer active, or expires_at has passed. |
| 422 | 200 VALIDATION_ERROR | Code rejected — unknown, inactive, outside its window, or used up. The message says which. |
Request
curl -X POST https://api.qariyb.com/api/v1/events/01HWX9KPQR/checkout/discount \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"cart_id": "01HWX9CART123",
"code": "RAMADAN10",
"payment_intent_id": "pi_3Abc123"
}'
Response
{
"success": true,
"code": 100,
"locale": "en",
"message": "OK",
"data": {
"code": "RAMADAN10",
"discount_type": 2,
"discount_type_name": "percentage",
"discount_value": 10,
"formatted_value": "10%",
"discount_amount": 50,
"new_total": 450,
"currency": "GBP"
}
}
Response (rejected)
{
"success": false,
"code": 200,
"locale": "en",
"message": "Invalid discount code.",
"data": null
}
Confirm checkout
Complete the checkout by confirming payment and providing attendee information. For paid events, include the Stripe PaymentIntent ID after successful payment.
Required attributes
- Name
cart_id- Type
- string
- Description
The cart ULID from the initialize response.
- Name
billing_name- Type
- string
- Description
Full name of the primary attendee (max 100 characters).
- Name
billing_email- Type
- string
- Description
Email address for order confirmation.
Conditional attributes
- Name
payment_intent_id- Type
- string
- Description
Stripe PaymentIntent ID (required for paid events, starts with
pi_). Not needed for free events.
- Name
billing_phone- Type
- string
- Description
Phone number of the attendee. Availability depends on the event's phone field setting:
required(must be provided),optional(nullable), oroff(not accepted). Max 20 characters.
Optional attributes
- Name
custom_answers- Type
- object
- Description
Answers to custom questions keyed by question ULID.
Response attributes
- Name
order- Type
- object
- Description
Order details including ID, status, and totals.
- Name
order.id- Type
- string
- Description
Order ULID.
- Name
order.status- Type
- string
- Description
Order status:
completedfor successful purchases,pendingfor events requiring approval. Other possible values:cancelled,refunded,failed,refund_pending.
- Name
order.total- Type
- integer
- Description
Total amount in cents (0 for free events).
- Name
order.currency- Type
- string
- Description
ISO 4217 currency code (e.g. USD).
- Name
order.billing_name- Type
- string
- Description
Billing name as submitted.
- Name
order.billing_email- Type
- string
- Description
Billing email as submitted.
- Name
order.requires_approval- Type
- boolean
- Description
Whether tickets require organizer approval before they are valid.
- Name
tickets- Type
- OrderTicket[]
- Description
Array of generated ticket objects. Each ticket has:
id(ULID),number(human-readable ticket reference, e.g.TK6K6V6NGKT2WZ),ticket_type(name string),status(purchasedorpending_approval),attendee_name(string — the buyer's name on the first ticket and the literal"Unassigned"on any ticket not yet allocated to a person; it is not null).There is no QR code or ticket URL in this response. Issue your own confirmation page, or subscribe to the
ticket.issuedwebhook.
- Name
requires_approval- Type
- boolean
- Description
Top-level convenience field, same value as
order.requires_approval. Whentrue, the order ispendingand tickets arepending_approval— the organizer must approve before tickets become valid.
Request
curl -X POST https://api.qariyb.com/api/v1/events/01HWX9KPQR/checkout/confirm \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"cart_id": "01HWX9CART1234567890ABCD",
"billing_name": "Ahmad Hassan",
"billing_email": "ahmad@example.com",
"billing_phone": "+1234567890",
"payment_intent_id": "pi_3ABC123XYZ",
"custom_answers": {
"01HWX9Q123": "No peanuts please",
"01HWX9Q456": "Social Media"
}
}'
Response (Paid Event)
{
"success": true,
"code": 100,
"locale": "en",
"message": "OK",
"data": {
"order": {
"id": "01HWX9ORDER123456789ABC",
"number": "ORD2026E2CJD6WN",
"status": "completed",
"subtotal": 5000,
"discount_amount": 0,
"total": 5000,
"currency": "USD",
"billing_name": "Ahmad Hassan",
"billing_email": "ahmad@example.com",
"requires_approval": false,
"created_at": "2026-10-21T21:34:54+00:00"
},
"tickets": [
{
"id": "01HWX9TKT1234567890ABCD",
"number": "TK6K6V6NGKT2WZ",
"ticket_type": "General Admission",
"status": "purchased",
"attendee_name": "Ahmad Hassan"
},
{
"id": "01HWX9TKT0987654321EFGH",
"number": "TKHRTYSX2NTNGF",
"ticket_type": "General Admission",
"status": "purchased",
"attendee_name": "Unassigned"
}
],
"requires_approval": false
}
}
Response (Free Event)
{
"success": true,
"code": 100,
"locale": "en",
"message": "OK",
"data": {
"order": {
"id": "01HWX9ORDER555555555DEF",
"status": "completed",
"total": 0,
"currency": "USD",
"billing_name": "Aisha Khan",
"billing_email": "aisha@example.com",
"requires_approval": false
},
"tickets": [
{
"id": "01HWX9TKT3333333333BBBB",
"ticket_type": "Free Admission",
"status": "purchased",
"attendee_name": null
}
],
"requires_approval": false
}
}
Response (Requires Approval)
{
"success": true,
"code": 100,
"locale": "en",
"message": "OK",
"data": {
"order": {
"id": "01HWX9ORDER987654321ZYX",
"status": "pending",
"total": 0,
"currency": "USD",
"billing_name": "Fatima Ali",
"billing_email": "fatima@example.com",
"requires_approval": true
},
"tickets": [
{
"id": "01HWX9TKT5555555555AAAA",
"ticket_type": "Free Admission",
"status": "pending_approval",
"attendee_name": null
}
],
"requires_approval": true
}
}
Collecting payment
This API is server-side only, so the card itself is never collected here. Your
backend calls initialize, hands the browser the client_secret it returns, and
the browser confirms the payment with Stripe directly. The secret key stays on
your server throughout.
browser your backend Qariyb
| "start checkout" --> |
| | initialize (sk_live_) --> |
| <-- client_secret --- | <-- cart_id + client_secret |
| |
| confirmPayment(client_secret) ----------------------> Stripe
| |
| "paid, here is the PI" --> |
| | confirm (sk_live_) --> |
| <-- order + tickets ----- | <-- order + tickets |
Never put your sk_live_ secret key in browser code. It is not scoped to a
single event or action: anyone who reads it can act as your organization. Keep
it on your server and expose only your own endpoints to the browser.
If you would rather not run a backend at all, use the
public checkout API with your pk_live_ publishable
key instead. That surface is built for the browser and carries a worked
Stripe.js and React example.
Stripe documentation
Error handling
Initialize errors
| HTTP | ApiCode | Scenario | Action |
|---|---|---|---|
| 401 | 606 | No secret key sent | Send your secret key as Authorization: Bearer sk_live_… |
| 401 | 607 | Key not valid for this surface | Use the secret key, not the pk_live_ publishable one |
| 403 | 500 | Event is draft or user lacks preview access | Event is not available for registration |
| 404 | 203 | Event ULID not found | Check the event ID |
| 422 | 200 | Event uses RSVP, not checkout | Use the /api/v1/events/{ulid}/rsvp endpoint |
| 422 | 405 | Validation error (invalid ticket, sold out, quantity limits, occurrence issues) | Display the error message to the user |
| 500 | 504 | Stripe PaymentIntent creation failed | Retry or contact support |
Confirm errors
| HTTP | ApiCode | Scenario | Action |
|---|---|---|---|
| 401 | 606 | No secret key sent | Send your secret key as Authorization: Bearer sk_live_… |
| 401 | 607 | Key not valid for this surface | Use the secret key, not the pk_live_ publishable one |
| 403 | 202 | Cart belongs to another user | The cart's user_id doesn't match the authenticated user |
| 403 | 500 | Event no longer published | Event state changed after cart was created |
| 404 | 506 | Cart not found or wrong event | Check cart_id and event ULID |
| 410 | 503 | Cart expired (see expires_at; 15 min default) | Restart the checkout flow |
| 422 | 200 | Validation error | Missing or invalid required fields |
| 402 | 504 | Stripe payment failed | PaymentIntent status is not succeeded |
| 402 | 505 | Payment amount mismatch | Stripe amount doesn't match expected total (possibly modified) |
Validation error messages
When the initialize endpoint returns HTTP 422, the message field contains one of these validation messages:
Ticket validation:
- "Invalid ticket type selected."
- "Tickets for {name} are sold out."
- "Tickets for {name} are not currently on sale."
Quantity validation:
- "Please select a ticket type."
- "Please specify the number of tickets."
- "You must purchase at least one ticket."
- "Minimum {n} tickets required for {name}."
- "Maximum {n} tickets allowed for {name}."
- "Only {n} tickets available for {name}."
Occurrence validation (recurring events):
- "Please select a date for this recurring event."
- "Invalid or unavailable date selected."
- "This date is sold out."
- "Only {n} spots remaining for this date."
Registration access:
- "Registration is closed." (or other closure reason from the event settings)