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?

POST/quote

Returns 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.

FieldRequiredNotes
dateYesYYYY-MM-DD
timeYesHH:mm, a bookable slot time (see Availability)
items[]YesAt 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[]NoUp to 50. Each: id and optional quantity (1 to 50). Unknown or inactive extras are 404 NOT_FOUND.
jsonPOST /quote body
{
  "date": "2026-11-14",
  "time": "10:00",
  "items": [
    { "experience": "supercar-drive", "optionId": "opt_5lap", "quantity": 2 }
  ],
  "extras": [
    { "id": "ext_photo_pack", "quantity": 1 }
  ]
}
json200 OK
{
  "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"
}
  • priceCents is the price after the date and time pricing rules; listPriceCents is the public price. pricingRule names the rule applied, or is null.
  • totals also carries netTotalCents on a net_rate account, or commissionCents on a list_plus_commission account. See Catalog.
  • capacity.ok is true only when the time is a bookable slot for every experience in the cart and the whole cart fits. When it is false, reason is the first failing check: closed, not_a_bookable_time, blocked, past_cutoff, sold_out or capacity (the whole-cart check).
jsoncapacity when it fails
"capacity": {
  "ok": false,
  "reason": "sold_out",
  "message": "That time is sold out.",
  "experience": "supercar-drive"
}

How do I create a booking?

POST/bookings

Takes 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.

FieldRequiredNotes
Idempotency-KeyYes (header)1 to 100 characters of A-Z a-z 0-9 _ . -, unique per booking you create.
customerYesname (up to 120) and email are required; phone (up to 40) optional.
drivers[]Yes1 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.
notesNoUp to 1000 characters, shown to the operator’s team on the booking.
sendCustomerEmailsNoDefault true. Set false to route this booking's confirmation to your notification address instead of the customer.
curl
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
jsonbooking.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
}
json201 Created
{
  "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.

json409
{
  "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.

json409 (credit 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 201 and the other 409 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.

ModeHow it worksStatus
on_accountInvoiced 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_linkThe 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

Pay-link settlement is not available together with net-rate pricing: that combination answers 409 UNAVAILABLE with details.reason: "pay_link_not_available". You can rehearse the whole flow with a test key; see Sandbox.

With a pay-link account, POST /bookings returns 201 with status: "awaiting_payment", paymentStatus: "pending", a payLinkUrl and a holdExpiresAt.

json201 Created (pay link)
{
  "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's holdMinutes (see GET /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. With sendCustomerEmails: false and no notification address nothing is emailed, so hand the customer payLinkUrl yourself.
  • Paid before the hold ends: status becomes confirmed and the confirmation email is sent.
  • Not paid in time: the seats are released within 2 minutes of holdExpiresAt and the booking reads expired (final). Book again if the customer still wants it.
  • Poll GET /bookings/{ref} to see the current status. payLinkUrl is only set while the booking is awaiting_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 /bookings answers 409 UNAVAILABLE with details.reason: "too_many_open_holds" (see the errors page for details.seatLimit, heldSeats and requestedSeats). 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: false on a booking. sendCustomerEmails can 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?

GET/bookings

Your 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
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"
json200 OK
{
  "ok": true,
  "data": {
    "bookings": [ { "bookingRef": "DR2601", "...": "same shape as a created booking" } ],
    "nextCursor": "eyJpZCI6IjEyMyJ9"
  },
  "requestId": "req_6a10ee"
}
GET/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?

POST/bookings/{ref}/cancel

Optional body: reason (up to 500 characters).

  • Free window. Cancelling at least freeCancelHours before the start (venue time; see GET /me) is free: refund: "none".
  • Later than that the booking is still cancelled, lateCancellation is true and refund is "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 UNAVAILABLE with details.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 refund is "none", even after the start time. A paid pay-link booking answers refund: "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 UNAVAILABLE with details.reason: "payment_in_progress". Retry shortly.
  • Cancelling is idempotent: a second call answers alreadyCancelled: true. No customer email is sent.
curl
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" }'
json200 OK
{
  "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"
}
json409 (already started)
{
  "ok": false,
  "error": {
    "code": "UNAVAILABLE",
    "message": "This booking has already started; contact us to cancel.",
    "details": { "reason": "started" }
  },
  "requestId": "req_0e4a3b"
}
json409 (customer is paying)
{
  "ok": false,
  "error": {
    "code": "UNAVAILABLE",
    "message": "The customer is paying right now. Try again shortly.",
    "details": { "reason": "payment_in_progress" }
  },
  "requestId": "req_3d58c0"
}