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:
- Initialize — create a cart, get a Stripe
client_secret(for paid events). - Apply a discount — optional, and only before the card is confirmed. This reprices the PaymentIntent so the card is charged the discounted amount.
- 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.
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_secretandconnected_account_id. Null for free events, where there is nothing to charge.connected_account_idis 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 nostripeAccountoption 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
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
| HTTP | ApiCode | Scenario |
|---|---|---|
| 401 | 600 / 601 | Missing or invalid widget key. |
| 403 | 604 API_KEY_ORIGIN_DENIED | Origin not in allowed_domains. |
| 404 | 500 EVENT_NOT_AVAILABLE | Event ULID not found or not published. |
| 404 | 501 TICKET_TYPE_NOT_FOUND | ticket_type_id doesn't belong to this event or is inactive. |
| 400 | 502 SOLD_OUT | Not enough seats left, counting those held in other shoppers' live carts. Two buyers racing for the last seat get this, not an error. |
| 422 | 200 VALIDATION_ERROR | Validation failed; per-field errors are at data.messages. RSVP events also return 422 here — use the RSVP endpoint instead. |
| 500 | 504 PAYMENT_FAILED | Stripe PaymentIntent creation failed. |
| 429 | 605 API_KEY_RATE_LIMITED | Per-key checkout rate exceeded (half the read rate — 60/min by default). |
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
| 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 guest 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/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
}
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).
Confirm takes no discount code. Apply one with Apply a discount before the card is confirmed; this endpoint reads the discount back off the PaymentIntent that call repriced. A discount_code sent here is ignored.
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
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
| HTTP | ApiCode | Scenario |
|---|---|---|
| 401 | 600 / 601 | Missing or invalid widget key. |
| 403 | 202 FORBIDDEN | The cart is user-owned (not a guest cart). |
| 403 | 500 EVENT_NOT_AVAILABLE | Event no longer published. |
| 404 | 506 CART_NOT_FOUND | cart_id doesn't match this event. |
| 410 | 503 CART_EXPIRED | Cart status not active or expires_at in the past. |
| 422 | 200 VALIDATION_ERROR | Field-level validation failed; per-field errors are at data.messages. |
| 422 | 505 PAYMENT_NOT_CONFIRMED | Missing/invalid payment_intent_id, or Stripe verification failed. |
| 429 | 605 API_KEY_RATE_LIMITED | Checkout 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:
- Initialize - Call Qariyb's initialize endpoint to create a cart and receive Stripe credentials
- Collect payment - Use the Payment Element to securely collect payment details
- Confirm with Stripe - Call
stripe.confirmPayment()to process the payment - Confirm with Qariyb - Send the PaymentIntent ID to Qariyb to complete the order
This example runs entirely in the browser, which is what this surface is for: every call uses your publishable pk_live_ key in the X-Widget-Key header. Publishable keys are safe to ship to visitors — they are origin-restricted and cannot read or change your organization's data.
Your sk_live_ secret key must never appear in browser code. If you want to drive checkout from a server instead, use the REST checkout API.
The client_secret returned from initialization is a sensitive value that authorizes the payment. Never log it or expose it in URLs.
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_secretfrom 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: