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

  1. Initialize - Create a cart with selected tickets, receive Stripe PaymentIntent
  2. Apply a discount - Optional, and only before the card is confirmed. Reprices the PaymentIntent so the card is charged the discounted amount
  3. Collect payment - Use Stripe.js to collect payment details
  4. Confirm - Submit payment confirmation and attendee information
  5. Receive tickets - Get order confirmation with generated tickets

POST/api/v1/events/:id/checkout/initialize

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, optional or off, from the event's registration settings. Decides whether billing_phone is 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_order and ≤ max_per_order for 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

POST
/api/v1/events/:id/checkout/initialize
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
}

POST/api/v1/events/:id/checkout/discount

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

    1 for a fixed amount, 2 for a percentage.

  • Name
    discount_type_name
    Type
    string
    Description

    Readable form: fixed_amount or percentage.

  • 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

HTTPApiCodeScenario
404500 EVENT_NOT_AVAILABLEEvent ULID not found, not published, or another organization's.
404506 CART_NOT_FOUNDcart_id is not a cart for this event.
410503 CART_EXPIREDCart is no longer active, or expires_at has passed.
422200 VALIDATION_ERRORCode rejected — unknown, inactive, outside its window, or used up. The message says which.

Request

POST
/api/v1/events/:id/checkout/discount
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
}

POST/api/v1/events/:id/checkout/confirm

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), or off (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: completed for successful purchases, pending for 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 (purchased or pending_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.issued webhook.

  • Name
    requires_approval
    Type
    boolean
    Description

    Top-level convenience field, same value as order.requires_approval. When true, the order is pending and tickets are pending_approval — the organizer must approve before tickets become valid.

Request

POST
/api/v1/events/:id/checkout/confirm
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         |

Stripe documentation


Error handling

Initialize errors

HTTPApiCodeScenarioAction
401606No secret key sentSend your secret key as Authorization: Bearer sk_live_…
401607Key not valid for this surfaceUse the secret key, not the pk_live_ publishable one
403500Event is draft or user lacks preview accessEvent is not available for registration
404203Event ULID not foundCheck the event ID
422200Event uses RSVP, not checkoutUse the /api/v1/events/{ulid}/rsvp endpoint
422405Validation error (invalid ticket, sold out, quantity limits, occurrence issues)Display the error message to the user
500504Stripe PaymentIntent creation failedRetry or contact support

Confirm errors

HTTPApiCodeScenarioAction
401606No secret key sentSend your secret key as Authorization: Bearer sk_live_…
401607Key not valid for this surfaceUse the secret key, not the pk_live_ publishable one
403202Cart belongs to another userThe cart's user_id doesn't match the authenticated user
403500Event no longer publishedEvent state changed after cart was created
404506Cart not found or wrong eventCheck cart_id and event ULID
410503Cart expired (see expires_at; 15 min default)Restart the checkout flow
422200Validation errorMissing or invalid required fields
402504Stripe payment failedPaymentIntent status is not succeeded
402505Payment amount mismatchStripe 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)

Was this page helpful?