Quickstart: storefront
This walkthrough makes the same calls the hosted booking widget makes. You need two things from the operator whose products you are selling: their tenant id and a publishable key (pk_…). The publishable key can only browse the catalog, read availability and open a hold; it cannot read bookings or move money, so it is safe to ship in a browser bundle.
export QS_BASE=https://quikstik-core.fly.devexport QS_TENANT_ID=00000000-0000-0000-0000-000000000001 # the operator's tenantexport QS_PUBLISHABLE_KEY=pk_live_… # their storefront keyEvery call below sends the same two headers:
x-api-key: $QS_PUBLISHABLE_KEYx-tenant-id: $QS_TENANT_ID1. List the catalog
Section titled “1. List the catalog”curl -s "$QS_BASE/offerings" \ -H "x-api-key: $QS_PUBLISHABLE_KEY" -H "x-tenant-id: $QS_TENANT_ID"{ "items": [ { "offering_id": "10000000-0000-0000-0000-0000000000a1", "catalog_product_id": "T1", "display_name": "Carriage Roads Guided E-Bike Tour", "kind": null }, { "offering_id": "30000000-0000-0000-0000-0000000000a3", "catalog_product_id": "R1", "display_name": "Carriage Roads Self-Guided E-Bike Rental", "kind": null } ], "next_cursor": null}An offering is a product; what you actually sell is one of its variants. Read the offering to get them:
curl -s "$QS_BASE/offerings/30000000-0000-0000-0000-0000000000a3" \ -H "x-api-key: $QS_PUBLISHABLE_KEY" -H "x-tenant-id: $QS_TENANT_ID"{ "offering_id": "30000000-0000-0000-0000-0000000000a3", "catalog_product_id": "R1", "display_name": "Carriage Roads Self-Guided E-Bike Rental", "slug": "ebike-rentals-carriage-roads", "variants": [ { "variant_id": "30000000-0000-0000-0000-0000000000b3", "display_name": "Carriage Roads Self-Guided E-Bike Rental", "sku": null } ]}GET /offerings/{offering_id}/variants/{variant_id} returns the variant in full: capacity, duration, meeting point, add-ons and the participant rules text.
2. Read availability for a day
Section titled “2. Read availability for a day”Availability is computed when you ask, from the tenant’s capacity chain (the departure, and every shared constraint above it such as a fleet or a permit hour). Ask for a variant and a window; you get one line per departure in the window.
curl -s "$QS_BASE/availability/snapshot-requests" \ -H "x-api-key: $QS_PUBLISHABLE_KEY" -H "x-tenant-id: $QS_TENANT_ID" \ -H "content-type: application/json" \ -d '{ "offering_variant_id": "30000000-0000-0000-0000-0000000000b3", "window": { "start": "2026-11-05T00:00:00Z", "end": "2026-11-06T00:00:00Z" }, "quantity": 2 }'{ "snapshot_id": "17007c51-5fbb-46c1-b0a6-fbc2e0662802", "generated_at_ms": 1793542016783, "window": { "start": "2026-11-05T00:00:00Z", "end": "2026-11-06T00:00:00Z" }, "lines": [ { "offering_variant_id": "30000000-0000-0000-0000-0000000000b3", "start_time": "2026-11-05T13:00:00Z", "status": "sold_out", "available_quantity": 0, "basis": { "chain_remaining": 0, "withheld": 0, "offered": 0 } }, { "offering_variant_id": "30000000-0000-0000-0000-0000000000b3", "start_time": "2026-11-05T14:00:00Z", "status": "available", "available_quantity": 6, "basis": { "chain_remaining": 6, "withheld": 0, "offered": 6 } } ]}Read status and available_quantity to render the picker. basis tells you why the number is what it is: chain_remaining is the tightest limit anywhere up the capacity chain, withheld is seats reserved for other sales channels, and offered = chain_remaining − withheld. An empty lines array means no departures are scheduled in that window.
A snapshot is advisory. It does not hold anything; the hold is the next step.
3. Hold the seats
Section titled “3. Hold the seats”A booking intent binds a variant and a start time to a departure and records what the customer wants. Create it as soon as the customer picks a departure, before you collect card details.
curl -s "$QS_BASE/booking-intents" \ -H "x-api-key: $QS_PUBLISHABLE_KEY" -H "x-tenant-id: $QS_TENANT_ID" \ -H "content-type: application/json" \ -d '{ "offering_variant_id": "30000000-0000-0000-0000-0000000000b3", "start_time": "2026-11-05T14:00:00Z", "quantity": 2, "participants": [ { "participant_id": "'$(uuidgen)'", "display_name": "Ana Rivera", "birth_date": "1988-04-02" }, { "participant_id": "'$(uuidgen)'", "display_name": "Mike Rivera", "birth_date": null } ] }'Each participant carries a participant_id you mint (any UUID); it is how later calls refer to that rider. The 201 response is the intent, including booking_intent_id and the departure_id it bound to. Keep the id: the purchase settles this hold, and the server commits capacity against the bound departure, so your client never names a capacity pool and cannot mis-key one.
4. Settle the purchase
Section titled “4. Settle the purchase”Tokenise the card with the processor’s client library (for Stripe, a PaymentMethod id from Stripe.js) and send the token with the hold. Quikstik commits the seats, resolves net, tax and gross from the governed catalog, then charges.
Mint an obligation_ref (a UUID) for the purchase. It names the booking’s money obligation from this moment on — balances, refunds and the ledger are all addressed by it — so store it with your order. Send a fresh x-idempotency-key per purchase attempt and reuse it when you retry that same attempt.
export OBLIGATION_REF=$(uuidgen)export IDEMPOTENCY_KEY=$(uuidgen)
curl -s "$QS_BASE/storefront/purchases" \ -H "x-api-key: $QS_PUBLISHABLE_KEY" -H "x-tenant-id: $QS_TENANT_ID" \ -H "x-idempotency-key: $IDEMPOTENCY_KEY" \ -H "content-type: application/json" \ -d '{ "booking_intent_id": "<from step 3>", "obligation_ref": "'$OBLIGATION_REF'", "offering_id": "30000000-0000-0000-0000-0000000000a3", "offering_variant_id": "30000000-0000-0000-0000-0000000000b3", "units": 2, "demand_class": "default", "settlement": "full", "effective_year": 2026, "effective_month": 11, "effective_day": 5, "provider": "stripe", "instrument_token": "pm_…" }'units is the number of seats to commit (normally the participant count). settlement is full or, where the tenant’s policy allows a deposit, deposit — the balance then becomes collectable later through a balance link. demand_class is the pricing lane; default is the direct storefront lane. For multi-day rentals add rental_days.
{ "outbox_id": "…", "processor_ref": "ch_…", "net_minor": 27800, "tax_minor": 1530, "gross_minor": 29330}Amounts are integer minor units (cents). Outcomes you must handle:
| Status | Reason | Meaning |
|---|---|---|
200 |
— | Seats committed and card settled. Show the confirmation. |
409 |
sold_out / capacity_full |
The chain could not hold the seats. Nothing was charged. Re-read availability. |
422 |
rail_refused |
The processor declined. Nothing is committed. Ask for another card. |
402 |
processor_not_configured |
The operator has not connected a processor. Nothing was charged. |
400 |
idempotency_key_required |
You forgot x-idempotency-key. |
Retrying a timed-out purchase with the same idempotency key is safe: you get the original result, not a second charge.
What happens next
Section titled “What happens next”The booking’s confirmation email leaves through Quikstik, with the operator in copy. The operator sees the booking in their app and on the day’s manifest. If the customer needs to cancel or move, they do it from the link in their confirmation (a guest link), and the refund fraction comes from the tenant’s cancellation policy, not from anything the storefront decides.
From here: Authentication if you are building for the operator side, or Conventions for headers, errors and pagination.