Developers
Quotes & bookings
Price a cart, create the booking, then read or cancel it. Bookings are idempotent, so a retry can never create a second one.
Updated 2026-10-20
How do I price a cart?
/quoteReturns prices, the concession fee, totals and a capacity answer for a cart. A quote is advisory: it holds nothing and the price can move before you book. Request bodies are limited to 64 KB.
| Field | Required | Notes |
|---|---|---|
date | Yes | YYYY-MM-DD |
time | Yes | HH:mm, a bookable slot time (see Availability) |
items[] | Yes | At least one. Each: experience (slug), optionId, optional quantity (1 to 20; expands into that many lines, one driver each) and optional driverIndex (0 to 49) to group lines driven by the same person. A line with driverIndex must have quantity 1. At most 50 lines after expansion. |
extras[] | No | Up to 50. Each: id and optional quantity (1 to 50). Unknown or inactive extras are 404 NOT_FOUND. |
{
"date": "2026-11-14",
"time": "10:00",
"items": [
{ "experience": "supercar-drive", "optionId": "opt_5lap", "quantity": 2 }
],
"extras": [
{ "id": "ext_photo_pack", "quantity": 1 }
]
}{
"ok": true,
"data": {
"date": "2026-11-14",
"time": "10:00",
"currency": "USD",
"pricingMode": "list_plus_commission",
"lines": [
{
"experience": "supercar-drive",
"optionId": "opt_5lap",
"optionName": "5 laps",
"isRideAlong": false,
"driverIndex": null,
"listPriceCents": 30000,
"priceCents": 30000,
"pricingRule": null
},
{
"experience": "supercar-drive",
"optionId": "opt_5lap",
"optionName": "5 laps",
"isRideAlong": false,
"driverIndex": null,
"listPriceCents": 30000,
"priceCents": 30000,
"pricingRule": null
}
],
"extras": [
{ "id": "ext_photo_pack", "name": "Photo pack", "quantity": 1, "priceCents": 2000 }
],
"fees": [
{ "label": "Concession fee", "amountCents": 5270 }
],
"totals": {
"subtotalCents": 62000,
"feeCents": 5270,
"totalCents": 67270,
"commissionCents": 6727
},
"capacity": { "ok": true }
},
"requestId": "req_e51c0a"
}priceCentsis the price after the date and time pricing rules;listPriceCentsis the public price.pricingRulenames the rule applied, or isnull.totalsalso carriesnetTotalCentson anet_rateaccount, orcommissionCentson alist_plus_commissionaccount. See Catalog.capacity.okistrueonly when the time is a bookable slot for every experience in the cart and the whole cart fits. When it isfalse,reasonis the first failing check:closed,not_a_bookable_time,blocked,past_cutoff,sold_outorcapacity(the whole-cart check).
"capacity": {
"ok": false,
"reason": "sold_out",
"message": "That time is sold out.",
"experience": "supercar-drive"
}How do I create a booking?
/bookingsTakes everything a quote takes (date, time, items, extras, same limits) plus the customer and the drivers. It validates the cart with the same checks as the operator's own checkout (including add-on eligibility) and re-checks capacity for the final cart before it creates anything.
| Field | Required | Notes |
|---|---|---|
Idempotency-Key | Yes (header) | 1 to 100 characters of A-Z a-z 0-9 _ . -, unique per booking you create. |
customer | Yes | name (up to 120) and email are required; phone (up to 40) optional. |
drivers[] | Yes | 1 to 50 entries: name required; email (waiver requests go here), phone, dob optional. Must cover every driverIndex used in items; lines without a driverIndex are numbered 0, 1, ... after the explicit ones. |
notes | No | Up to 1000 characters, shown to the operator’s team on the booking. |
sendCustomerEmails | No | Default true. Set false to route this booking's confirmation to your notification address instead of the customer. |
curl -X POST https://<your-booking-domain>/api/partner/v1/bookings \
-H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: order-48213" \
-H "Content-Type: application/json" \
-d @booking.json{
"date": "2026-11-14",
"time": "10:00",
"items": [
{ "experience": "supercar-drive", "optionId": "opt_5lap", "quantity": 2 }
],
"extras": [
{ "id": "ext_photo_pack", "quantity": 1 }
],
"customer": {
"name": "Ann Lee",
"email": "ann@example.com",
"phone": "+1 702 555 0100"
},
"drivers": [
{ "name": "Ann Lee", "email": "ann@example.com", "dob": "1988-04-02" },
{ "name": "Ben Ortiz", "email": "ben@example.com" }
],
"notes": "Birthday group, please seat them together.",
"sendCustomerEmails": true
}{
"ok": true,
"data": {
"bookingRef": "DR2601",
"idempotencyKey": "order-48213",
"sandbox": false,
"status": "confirmed",
"bookingStatus": "scheduled",
"paymentStatus": "invoiced_pending",
"settlementMode": "on_account",
"date": "2026-11-14",
"time": "10:00",
"customer": { "name": "Ann Lee", "email": "ann@example.com", "phone": "+1 702 555 0100" },
"drivers": [
{ "index": 0, "name": "Ann Lee", "email": "ann@example.com", "phone": null },
{ "index": 1, "name": "Ben Ortiz", "email": "ben@example.com", "phone": null }
],
"items": [
{
"experience": "supercar-drive",
"optionId": "opt_5lap",
"optionName": "5 laps",
"driverIndex": 0,
"isRideAlong": false,
"priceCents": 30000
},
{
"experience": "supercar-drive",
"optionId": "opt_5lap",
"optionName": "5 laps",
"driverIndex": 1,
"isRideAlong": false,
"priceCents": 30000
}
],
"extras": [
{ "id": "ext_photo_pack", "name": "Photo pack", "quantity": 1, "priceCents": 2000 }
],
"totals": {
"subtotalCents": 62000,
"feeCents": 5270,
"totalCents": 67270,
"commissionCents": 6727
},
"payLinkUrl": null,
"holdExpiresAt": null,
"waivers": [
{ "driverIndex": 0, "driver": "Ann Lee", "status": "pending", "url": "https://booking.example.com/waiver/k3x9" },
{ "driverIndex": 1, "driver": "Ben Ortiz", "status": "pending", "url": "https://booking.example.com/waiver/k3x9" }
],
"createdAt": "2026-10-20T17:42:10.000Z",
"updatedAt": "2026-10-20T17:42:10.000Z",
"replayed": false
},
"requestId": "req_2c7f90"
}status is one of confirmed, awaiting_payment, payment_declined, expired or cancelled. waivers[].url is the booking's forwardable waiver link (the same for every driver). No SMS is ever sent for an API booking.
Creating a booking can fail
Common answers: 400 VALIDATION (bad cart, missing header), 403 FORBIDDEN (experience not enabled for you), 404 NOT_FOUND (unknown experience or extra), 409 UNAVAILABLE (the time can no longer be booked), 409 CREDIT_LIMIT (on-account accounts) and 409 DUPLICATE. When you get UNAVAILABLE, details.reason says why; refresh availability and offer another time.
{
"ok": false,
"error": {
"code": "UNAVAILABLE",
"message": "That time is sold out.",
"details": { "reason": "sold_out", "experience": "supercar-drive" }
},
"requestId": "req_b83d11"
}CREDIT_LIMIT means the booking would take what you owe on account over your credit limit. Nothing is created. Do not retry it automatically: retry with the same Idempotency-Key once an invoice is paid or the operator raises your limit.
{
"ok": false,
"error": {
"code": "CREDIT_LIMIT",
"message": "This booking ($1,298.00) would take your on-account balance ($24,120.00 open or invoiced) over your credit limit of $25,000.00. Pay an open invoice or contact us, then retry.",
"details": {
"reason": "credit_limit",
"limitCents": 2500000,
"openCents": 2412000,
"requestedCents": 129800
}
},
"requestId": "req_c4e207"
}What is the Idempotency-Key for?
- Generate one unique key per booking you intend to create (for example your own order id), and reuse it on every retry of that same booking.
- Sending the same key again returns the original booking with 200 and
replayed: true, whatever the body says. - If two first requests with the same key run at the same moment, one gets
201and the other409 DUPLICATE; retry the second to read the result. - Keys are scoped to your account and to the key mode, so test and live never collide.
Who pays: me or the customer?
Your account has one settlement mode, shown as settlementMode in GET /me.
| Mode | How it works | Status |
|---|---|---|
on_account | Invoiced to you. The booking is confirmed straight away and goes onto your monthly statement (paymentStatus: invoiced_pending). Nothing is collected from the customer through the API. Bookings are accepted up to your credit limit (creditLimitCents in GET /me): one that would take openCents plus its total over the limit is refused with 409 CREDIT_LIMIT. | Available now |
pay_link | The customer pays on our secure hosted payment page. The booking is created awaiting_payment, the seats are held for you until holdExpiresAt, and the response carries a payLinkUrl. No API route accepts card data. | Available now |
Note
409 UNAVAILABLE with details.reason: "pay_link_not_available". You can rehearse the whole flow with a test key; see Sandbox.How does a pay-link booking work?
With a pay-link account, POST /bookings returns 201 with status: "awaiting_payment", paymentStatus: "pending", a payLinkUrl and a holdExpiresAt.
{
"ok": true,
"data": {
"bookingRef": "DR2602",
"idempotencyKey": "order-48215",
"sandbox": false,
"status": "awaiting_payment",
"bookingStatus": "scheduled",
"paymentStatus": "pending",
"settlementMode": "pay_link",
"date": "2026-11-14",
"time": "10:00",
"payLinkUrl": "https://booking.example.com/pay/<token>",
"holdExpiresAt": "2026-10-20T18:12:10.000Z",
"waivers": [
{ "driverIndex": 0, "driver": "Ann Lee", "status": "pending", "url": "https://booking.example.com/waiver/m7q2" }
],
"replayed": false,
"...": "remaining fields as in an on-account booking"
},
"requestId": "req_a94e17"
}- Seats are held. Nobody else can take them until
holdExpiresAt. The hold lasts your account'sholdMinutes(seeGET /me) and never runs past the start time. - The customer pays on our secure hosted payment page at
payLinkUrl. We email the link to the customer, or to your notification address when confirmations are routed to you. WithsendCustomerEmails: falseand no notification address nothing is emailed, so hand the customerpayLinkUrlyourself. - Paid before the hold ends:
statusbecomesconfirmedand the confirmation email is sent. - Not paid in time: the seats are released within 2 minutes of
holdExpiresAtand the booking readsexpired(final). Book again if the customer still wants it. - Poll
GET /bookings/{ref}to see the currentstatus.payLinkUrlis only set while the booking isawaiting_payment. - An unpaid pay-link booking cannot be rescheduled before it is paid, by you or by the operator's staff. Cancel it and book again.
- To protect inventory, one account can have at most 25 unpaid pay-link bookings, or 100 driving seats across them, holding seats at a time. Past that,
POST /bookingsanswers409 UNAVAILABLEwithdetails.reason: "too_many_open_holds"(see the errors page fordetails.seatLimit,heldSeatsandrequestedSeats). Wait for customers to pay, or cancel unpaid bookings.
Who gets the confirmation email?
- By default the customer.
- Your notification address instead, when your account is set to route confirmations to you, or when you send
sendCustomerEmails: falseon a booking.sendCustomerEmailscan only narrow: it cannot turn customer emails back on for an account routed to you. - Waiver requests, pre-visit reminders and other follow-up emails still go to the drivers and the customer.
- No SMS is sent for API bookings.
How do I list and read my bookings?
/bookingsYour own API bookings, newest first. Filter by experience date with from and to (both inclusive), and page with limit (1 to 100, default 50) and cursor (the previous page's nextCursor).
curl "https://<your-booking-domain>/api/partner/v1/bookings?from=2026-11-01&to=2026-11-30&limit=50" \
-H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx"{
"ok": true,
"data": {
"bookings": [ { "bookingRef": "DR2601", "...": "same shape as a created booking" } ],
"nextCursor": "eyJpZCI6IjEyMyJ9"
},
"requestId": "req_6a10ee"
}/bookings/{ref}One booking by its bookingRef (DR... live, TST-... sandbox). A key only ever reads and cancels bookings that its own account created through this API, in its own mode; anything else is 404 NOT_FOUND.
How do I cancel a booking?
/bookings/{ref}/cancelOptional body: reason (up to 500 characters).
- Free window. Cancelling at least
freeCancelHoursbefore the start (venue time; seeGET /me) is free:refund: "none". - Later than that the booking is still cancelled,
lateCancellationistrueandrefundis"manual": the operator's team reviews any fee or refund with you. - Already started. A paid or on-account live booking whose start time has passed cannot be cancelled through the API:
409 UNAVAILABLEwithdetails.reason: "started". Contact us. - An on-account booking leaves your open statement when it is cancelled.
- Unpaid pay-link booking. Cancelled at once: the held seats are released, the pay link is closed and
refundis"none", even after the start time. A paid pay-link booking answersrefund: "manual": the operator's team refunds the customer, and nothing is refunded automatically. - If the customer is paying at that very moment, the cancel answers
409 UNAVAILABLEwithdetails.reason: "payment_in_progress". Retry shortly. - Cancelling is idempotent: a second call answers
alreadyCancelled: true. No customer email is sent.
curl -X POST https://<your-booking-domain>/api/partner/v1/bookings/DR2601/cancel \
-H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "reason": "Guest changed plans" }'{
"ok": true,
"data": {
"booking": {
"bookingRef": "DR2601",
"status": "cancelled",
"bookingStatus": "cancelled",
"...": "same shape as a created booking"
},
"alreadyCancelled": false,
"refund": "none",
"lateCancellation": false,
"hoursBeforeStart": 120,
"freeCancelHours": 48
},
"requestId": "req_f20b4d"
}{
"ok": false,
"error": {
"code": "UNAVAILABLE",
"message": "This booking has already started; contact us to cancel.",
"details": { "reason": "started" }
},
"requestId": "req_0e4a3b"
}{
"ok": false,
"error": {
"code": "UNAVAILABLE",
"message": "The customer is paying right now. Try again shortly.",
"details": { "reason": "payment_in_progress" }
},
"requestId": "req_3d58c0"
}