Public Checkout API

Public checkout creates guest carts and orders — no user authentication required. Carts created here are not tied to any user account and are scoped to the widget key that opened them.

The flow is:

  1. Initialize — create a cart, get a Stripe client_secret (for paid events).
  2. Apply a discount — optional, and only before the card is confirmed. This reprices the PaymentIntent so the card is charged the discounted amount.
  3. Confirm — verify payment with Stripe and finalize the order.

All three endpoints authenticate with the publishable widget key in the X-Widget-Key header, and are designed to be called from the browser — this is the surface Qariyb Elements itself uses.

If your checkout runs on your own server instead, use the REST checkout API with your secret key. See which key, which API if you are not sure which fits.

Responses put a single resource directly on data; there is no item wrapper.


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

Initialize checkout

Creates a guest cart and (for paid events) a Stripe PaymentIntent. RSVP events (has_tickets=false, pricing_model=free) must use /rsvp instead.

Path parameters

  • Name
    id
    Type
    string
    Description
    The ULID of the event.

Request body

  • Name
    ticket_type_id
    Type
    string
    Description

    ULID of an active ticket type on this event.

  • Name
    quantity
    Type
    integer
    Description

    1–10. Also validated against the ticket type's min_tickets_per_order and max_tickets_per_order configuration.

  • Name
    occurrence_id
    Type
    string?
    Description

    For recurring events: ULID of the specific occurrence. Validated against the event's occurrences relationship.

Response fields

  • Name
    cart_id
    Type
    string
    Description
    Cart ULID — pass back on confirm.
  • Name
    stripe
    Type
    object | null
    Description

    For paid events: publishable_key, client_secret and connected_account_id. Null for free events, where there is nothing to charge.

    connected_account_id is non-null when the organizer is on Stripe Standard, which takes direct charges. Pass it to Stripe.js as { stripeAccount: connected_account_id } when you load it — otherwise confirming the payment fails with a 404, because the PaymentIntent lives on the organizer's account rather than the platform's. It is null for destination-charge organizers, where no stripeAccount option is needed.

  • Name
    amount
    Type
    integer
    Description
    Cart subtotal in minor units.
  • Name
    currency
    Type
    string
    Description
    3-letter ISO 4217 code, uppercase.
  • Name
    is_free_event
    Type
    boolean
    Description

    true when amount is zero. Skip Stripe and proceed straight to confirm.

  • Name
    custom_questions
    Type
    CustomQuestion[]
    Description

    Ordered by display_order. Empty array when none configured.

  • Name
    phone_field_type
    Type
    'required' | 'optional' | 'off'
    Description

    How the widget should render the phone field. Resolved from the event's settings.registration.phone_field_type (default 'optional').

  • Name
    expires_at
    Type
    string
    Description
    Cart expiry timestamp (ISO 8601).

Request

POST
/api/v1/public/events/:id/checkout/initialize
curl -X POST https://api.qariyb.com/api/v1/public/events/01HWX9KPQR/checkout/initialize \
  -H "X-Widget-Key: pk_live_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "ticket_type_id": "01HWX9TKT123",
    "quantity": 2
  }'

Response (paid)

{
  "success": true,
  "code": 100,
  "data": {
    "cart_id": "01HWX9CART01",
    "stripe": {
      "publishable_key": "pk_live_stripe...",
      "client_secret": "pi_3..._secret_...",
      "connected_account_id": null
    },
    "amount": 3000,
    "currency": "GBP",
    "is_free_event": false,
    "custom_questions": [],
    "phone_field_type": "optional",
    "expires_at": "2026-06-16T18:00:00+00:00"
  }
}

Response (free)

{
  "success": true,
  "code": 100,
  "data": {
    "cart_id": "01HWX9CART02",
    "stripe": null,
    "amount": 0,
    "currency": "USD",
    "is_free_event": true,
    "custom_questions": [],
    "phone_field_type": "optional",
    "expires_at": "2026-06-16T18:00:00+00:00"
  }
}

Initialize errors

HTTPApiCodeScenario
401600 / 601Missing or invalid widget key.
403604 API_KEY_ORIGIN_DENIEDOrigin not in allowed_domains.
404500 EVENT_NOT_AVAILABLEEvent ULID not found or not published.
404501 TICKET_TYPE_NOT_FOUNDticket_type_id doesn't belong to this event or is inactive.
400502 SOLD_OUTNot enough seats left, counting those held in other shoppers' live carts. Two buyers racing for the last seat get this, not an error.
422200 VALIDATION_ERRORValidation failed; per-field errors are at data.messages. RSVP events also return 422 here — use the RSVP endpoint instead.
500504 PAYMENT_FAILEDStripe PaymentIntent creation failed.
429605 API_KEY_RATE_LIMITEDPer-key checkout rate exceeded (half the read rate — 60/min by default).

POST/api/v1/public/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.

Applying a second code recomputes from the cart subtotal rather than stacking. There is no "remove": the intent would still be repriced while confirm checked it against the full amount, so apply a different code instead.

Request body

  • Name
    cart_id
    Type
    string
    Description

    ULID returned from initialize. Must be a guest cart for this event, active, and not expired.

  • Name
    code
    Type
    string
    Description

    The discount code. Up to 50 chars. Matched case-insensitively; the response echoes the canonical casing.

  • Name
    payment_intent_id
    Type
    string
    Description

    The PaymentIntent to reprice. Must start with pi_.

Response fields

  • Name
    code
    Type
    string
    Description
    The code as stored, which may differ in case from what was typed.
  • Name
    discount_type
    Type
    integer
    Description
    1 fixed amount, 2 percentage.
  • Name
    discount_type_name
    Type
    string
    Description
    '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, 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.

Errors

HTTPApiCodeScenario
404500 EVENT_NOT_AVAILABLEEvent ULID not found, not published, or another organization's.
404506 CART_NOT_FOUNDcart_id is not a guest 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/public/events/:id/checkout/discount
curl -X POST https://api.qariyb.com/api/v1/public/events/01HWX9KPQR/checkout/discount \
  -H "X-Widget-Key: pk_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": 100,
    "new_total": 900,
    "currency": "GBP"
  }
}

Response (rejected)

{
  "success": false,
  "code": 200,
  "locale": "en",
  "message": "Invalid discount code.",
  "data": null
}

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

Confirm checkout

Verifies payment with Stripe (for paid events) and finalizes the order. For free events, it just creates the order.

Confirm is safe to retry. Calling it again with the same cart_id and the same payment_intent_id returns the same order rather than charging again or creating a second order, so a dropped connection or an impatient second click does not cost the buyer a ticket. Retrying with a different payment_intent_id is refused with HTTP 410 (503), so a stale cart cannot be attached to someone else's payment.

There is no endpoint that reads an order back by id. If you need the order after the fact, keep the confirm response, repeat the confirm call with the same two values, or subscribe to the order.completed webhook.

Request body

  • Name
    cart_id
    Type
    string
    Description

    ULID returned from initialize. Must belong to a guest cart for this event, must be active, and must not be expired.

  • Name
    payment_intent_id
    Type
    string?
    Description

    Stripe PaymentIntent ID. Required for paid events; must start with pi_.

  • Name
    billing_name
    Type
    string
    Description
    Max 100 chars.
  • Name
    billing_email
    Type
    string
    Description
    Valid email, max 255 chars.
  • Name
    billing_phone
    Type
    string?
    Description

    Required when the cart's phone_field_type is 'required'. Optional otherwise — a phone sent while the field is 'off' is accepted and ignored, not rejected. Max 20 characters.

  • Name
    custom_answers
    Type
    object?
    Description

    Map of questionUlid to a string or string[]. Validated against the event's custom questions (required questions, allowed option values, checkbox-array support).

Response fields

Returns the order plus generated tickets. requires_approval is mirrored at both order.requires_approval and the top level so widgets can branch on either.

  • Name
    order.id
    Type
    string
    Description

    Order ULID — the stable key, not the reference a buyer quotes.

  • Name
    order.number
    Type
    string
    Description

    Human-readable order number, e.g. ORD2026HTP9PWGC. This is the one the organizer sees, so show this on a confirmation screen rather than the ULID.

  • Name
    order.status
    Type
    string
    Description
    Lower-cased OrderStatus enum value.
  • Name
    order.subtotal
    Type
    integer
    Description
    Ticket total before any discount, in minor units.
  • Name
    order.discount_amount
    Type
    integer
    Description
    Amount taken off by a discount code, in minor units; 0 when none.
  • Name
    order.total
    Type
    integer
    Description
    Total in minor units.
  • Name
    order.currency
    Type
    string
    Description
    Uppercase ISO 4217.
  • Name
    order.billing_name
    Type
    string
    Description
    From the request.
  • Name
    order.billing_email
    Type
    string
    Description
    From the request.
  • Name
    order.requires_approval
    Type
    boolean
    Description
    Mirror of the top-level flag.
  • Name
    order.created_at
    Type
    string
    Description
    ISO 8601 datetime the order was placed.
  • Name
    tickets[].id
    Type
    string
    Description
    Ticket ULID.
  • Name
    tickets[].number
    Type
    string
    Description

    Human-readable ticket number, e.g. TKZHHQ6RD8E4VQ.

  • Name
    tickets[].ticket_type
    Type
    string
    Description
    Ticket-type name ('Standard' if missing).
  • Name
    tickets[].status
    Type
    string
    Description
    Lower-cased TicketStatus enum value.
  • Name
    tickets[].attendee_name
    Type
    string
    Description

    The buyer's name on the first ticket, and the literal "Unassigned" on any ticket not yet allocated to a person. It is never null.

  • Name
    requires_approval
    Type
    boolean
    Description
    Top-level mirror.

Request

POST
/api/v1/public/events/:id/checkout/confirm
curl -X POST https://api.qariyb.com/api/v1/public/events/01HWX9KPQR/checkout/confirm \
  -H "X-Widget-Key: pk_live_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "cart_id": "01HWX9CART01",
    "payment_intent_id": "pi_3NkF...",
    "billing_name": "Aisha Khan",
    "billing_email": "aisha@example.com"
  }'

Response (completed)

{
  "success": true,
  "code": 100,
  "locale": "en",
  "message": "OK",
  "data": {
    "order": {
      "id": "01HWX9ORD01",
      "number": "ORD2026E2CJD6WN",
      "status": "completed",
      "subtotal": 3000,
      "discount_amount": 0,
      "total": 3000,
      "currency": "GBP",
      "billing_name": "Aisha Khan",
      "billing_email": "aisha@example.com",
      "requires_approval": false,
      "created_at": "2026-10-21T21:34:54+00:00"
    },
    "tickets": [
      {
        "id": "01HWX9TIX01",
        "number": "TK6K6V6NGKT2WZ",
        "ticket_type": "Individual Ticket",
        "status": "purchased",
        "attendee_name": "Aisha Khan"
      }
    ],
    "requires_approval": false
  }
}

Response (pending approval)

{
  "success": true,
  "code": 100,
  "locale": "en",
  "message": "OK",
  "data": {
    "order": {
      "id": "01HWX9ORD02",
      "number": "ORD2026F3DKE7XP",
      "status": "pending",
      "subtotal": 3000,
      "discount_amount": 0,
      "total": 3000,
      "currency": "GBP",
      "billing_name": "Aisha Khan",
      "billing_email": "aisha@example.com",
      "requires_approval": true,
      "created_at": "2026-10-21T21:34:54+00:00"
    },
    "tickets": [
      {
        "id": "01HWX9TIX02",
        "number": "TKHRTYSX2NTNGF",
        "ticket_type": "Individual Ticket",
        "status": "pending_approval",
        "attendee_name": "Unassigned"
      }
    ],
    "requires_approval": true
  }
}

Confirm errors

HTTPApiCodeScenario
401600 / 601Missing or invalid widget key.
403202 FORBIDDENThe cart is user-owned (not a guest cart).
403500 EVENT_NOT_AVAILABLEEvent no longer published.
404506 CART_NOT_FOUNDcart_id doesn't match this event.
410503 CART_EXPIREDCart status not active or expires_at in the past.
422200 VALIDATION_ERRORField-level validation failed; per-field errors are at data.messages.
422505 PAYMENT_NOT_CONFIRMEDMissing/invalid payment_intent_id, or Stripe verification failed.
429605 API_KEY_RATE_LIMITEDCheckout rate exceeded.

Stripe.js integration

For paid events, use Stripe.js and the Payment Element to securely collect payment information. The Payment Element is a pre-built UI component that supports cards, digital wallets, and 100+ global payment methods.

Installation

Install the required packages in your React application:

npm install @stripe/stripe-js @stripe/react-stripe-js

How it works

The integration follows a two-phase flow that coordinates between Qariyb and Stripe:

  1. Initialize - Call Qariyb's initialize endpoint to create a cart and receive Stripe credentials
  2. Collect payment - Use the Payment Element to securely collect payment details
  3. Confirm with Stripe - Call stripe.confirmPayment() to process the payment
  4. Confirm with Qariyb - Send the PaymentIntent ID to Qariyb to complete the order

Step 1: Setup and types

First, install the required packages and set up your imports and TypeScript interfaces:

import { useState, useEffect } from 'react'
import { loadStripe, Stripe } from '@stripe/stripe-js'
import {
  Elements,
  PaymentElement,
  useStripe,
  useElements,
} from '@stripe/react-stripe-js'

interface CustomQuestion {
  id: string
  question: string
  type: string
  required: boolean
  options: string[]
  display_order: number
}

interface CheckoutData {
  cart_id: string
  // null for a free ticket: there is nothing to charge, so skip Stripe entirely.
  stripe: {
    publishable_key: string
    client_secret: string
    // Set when the organizer is on Stripe Standard (direct charges). Pass it to
    // loadStripe as { stripeAccount } or confirming the payment returns 404.
    connected_account_id: string | null
  } | null
  amount: number
  currency: string
  is_free_event: boolean
  // Render one field per question. Key custom_answers by `id` when you confirm.
  custom_questions: CustomQuestion[]
  phone_field_type: 'required' | 'optional' | 'off'
  expires_at: string
}

Step 2: Initialize checkout

Create a component that initializes the checkout by calling the Qariyb API. This fetches the cart and Stripe credentials, then loads Stripe.js with the organizer's publishable key.

export default function EventCheckout({ eventId, ticketTypeId, quantity }) {
  const [checkoutData, setCheckoutData] = useState<CheckoutData | null>(null)
  const [stripePromise, setStripePromise] = useState<Promise<Stripe | null> | null>(null)

  useEffect(() => {
    async function initCheckout() {
      // Publishable key, public surface: safe to run in the browser.
      const response = await fetch(
        `https://api.qariyb.com/api/v1/public/events/${eventId}/checkout/initialize`,
        {
          method: 'POST',
          headers: {
            'X-Widget-Key': 'pk_live_...',
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            ticket_type_id: ticketTypeId,
            quantity: quantity,
          }),
        }
      )

      const { data: checkout } = await response.json()
      setCheckoutData(checkout)

      // A free ticket carries no PaymentIntent: skip Stripe and confirm directly.
      if (checkout.is_free_event) {
        return
      }

      // connected_account_id is set for Standard (direct charge) organizers and
      // must be passed here, or confirming the payment fails with a 404.
      setStripePromise(
        loadStripe(
          checkout.stripe.publishable_key,
          checkout.stripe.connected_account_id
            ? { stripeAccount: checkout.stripe.connected_account_id }
            : undefined
        )
      )
    }

    initCheckout()
  }, [eventId, ticketTypeId, quantity])

  if (!checkoutData || !stripePromise) {
    return <div>Loading checkout...</div>
  }

  return (
    <Elements
      stripe={stripePromise}
      options={{
        clientSecret: checkoutData.stripe.client_secret,
        appearance: { theme: 'stripe' },
      }}
    >
      <CheckoutForm
        cartId={checkoutData.cart_id}
        eventId={eventId}
        onSuccess={(order) => console.log('Order completed:', order)}
      />
    </Elements>
  )
}

Step 3: Payment form component

Create the checkout form that collects attendee information and payment details. This component must be rendered inside the Elements provider.

function CheckoutForm({
  cartId,
  eventId,
  onSuccess,
}: {
  cartId: string
  eventId: string
  onSuccess: (order: any) => void
}) {
  const stripe = useStripe()
  const elements = useElements()
  const [error, setError] = useState<string | null>(null)
  const [processing, setProcessing] = useState(false)
  const [billingName, setBillingName] = useState('')
  const [billingEmail, setBillingEmail] = useState('')

  // Form JSX shown in next step...
}

Step 4: Handle form submission

The submit handler validates the payment element, confirms the payment with Stripe, then confirms the order with Qariyb.

const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault()

  if (!stripe || !elements) return

  setProcessing(true)
  setError(null)

  // Validate the Payment Element
  const { error: submitError } = await elements.submit()
  if (submitError) {
    setError(submitError.message ?? 'Validation failed')
    setProcessing(false)
    return
  }

  // Confirm payment with Stripe
  const { error: stripeError, paymentIntent } = await stripe.confirmPayment({
    elements,
    redirect: 'if_required',
    confirmParams: {
      return_url: `${window.location.origin}/checkout/complete`,
    },
  })

  if (stripeError) {
    setError(stripeError.message ?? 'Payment failed')
    setProcessing(false)
    return
  }

  // Confirm checkout with Qariyb API, again on the public surface with the
  // publishable key. Confirm is safe to retry: calling it a second time with the
  // same cart_id and payment_intent_id returns the same order rather than
  // charging again, so a dropped connection does not cost the buyer a ticket.
  const response = await fetch(
    `https://api.qariyb.com/api/v1/public/events/${eventId}/checkout/confirm`,
    {
      method: 'POST',
      headers: {
        'X-Widget-Key': 'pk_live_...',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        cart_id: cartId,
        billing_name: billingName,
        billing_email: billingEmail,
        payment_intent_id: paymentIntent.id,
      }),
    }
  )

  const result = await response.json()

  if (result.success) {
    onSuccess(result.data)
  } else {
    setError(result.message ?? 'Order confirmation failed')
  }

  setProcessing(false)
}

Step 5: Render the form

Finally, render the form with attendee inputs and the Stripe Payment Element:

return (
  <form onSubmit={handleSubmit}>
    <input
      type="text"
      placeholder="Full name"
      value={billingName}
      onChange={(e) => setBillingName(e.target.value)}
      required
    />
    <input
      type="email"
      placeholder="Email address"
      value={billingEmail}
      onChange={(e) => setBillingEmail(e.target.value)}
      required
    />

    <PaymentElement options={{ layout: 'tabs' }} />

    {error && <div className="error">{error}</div>}

    <button type="submit" disabled={!stripe || processing}>
      {processing ? 'Processing...' : 'Complete Purchase'}
    </button>
  </form>
)

Key concepts

  • Name
    PaymentIntent
    Description

    A Stripe object representing a payment attempt. The client_secret from Qariyb's initialize response is used to authenticate the payment on the client side.

  • Name
    Payment Element
    Description

    A pre-built, secure UI component that collects payment details. It automatically handles card validation, displays appropriate payment methods, and supports 3D Secure authentication.

  • Name
    redirect: 'if_required'
    Description

    Keeps users on your page for simple card payments, but redirects when required for 3D Secure verification or bank authentication.

  • Name
    elements.submit()
    Description

    Validates the Payment Element before confirming. Always call this before stripe.confirmPayment() to catch validation errors early.

Stripe documentation

For more details on the Payment Element and Stripe.js, see:


Was this page helpful?