Developers

Sandbox certification

Before the operator turns on live keys, you run a short checklist of practice calls with a test key. Each step ticks itself off as you make the call.

Updated 2026-10-20

Why is there a certification?

Live keys make real bookings that hold real seats and appear on a real statement. The checklist proves your integration handles the normal path and the awkward cases (sold out, declined payment, retries) before it touches any of that. Nothing here costs money or holds capacity: it all uses a test key.

How does it work?

  • Use a test key (pk_test_...). Calls made with a live key do not count.
  • Make each call below. The step passes automatically the first time it succeeds. You do not submit anything.
  • Steps can be done in any order, but the order below is the easiest.
  • When every required step has passed, tell the operator. They confirm the certification and switch on live access for your account.

Note

Progress is read from your test-key calls in the last 30 days, including calls made with a key you have since rotated. Practice bookings are deleted after 7 days, which does not remove a step you passed. Once the operator confirms your certification, it stays confirmed. See Sandbox for what test keys do.

What are the steps?

Paths below are relative to /api/partner/v1. Replace the experience slug, option id, date and time with values from your own catalogue, and use a date and time that is open (see Availability).

StepRequiredThe callPasses when
catalogYesGET /experiencesIt answers 200.
experienceYesGET /experiences/{slug}It answers 200 for one slug from the list.
availabilityYesGET /availability?experience={slug}&from={date}&to={date}It answers 200.
slotsYesGET /availability/slots?experience={slug}&date={date}It answers 200.
quoteYesPOST /quoteIt answers 200.
booking_createdYesPOST /bookings with an Idempotency-KeyIt answers 201 and returns a TST- reference.
booking_fetchedYesGET /bookings/{ref}It answers 200 for one of your bookings.
booking_listedYesGET /bookingsIt answers 200.
pay_successYes, if you settle by pay linkOpen a booking's payLinkUrl in a browser and choose Simulate successful paymentThe sandbox pay page records the payment.
pay_declinedYes, if you settle by pay linkOpen a booking's payLinkUrl and choose Simulate declined card (or book with +declined in the customer email)The sandbox pay page records the decline.
sold_out_handledYesPOST /bookings with +soldout in the customer emailIt answers 409 UNAVAILABLE.
duplicate_handledYesPOST /bookings with +duplicate in the customer emailIt answers 409 DUPLICATE.
replayYesSend the same POST /bookings twice with the same Idempotency-Key and bodyThe second call answers 200 with replayed: true.
cancelYesPOST /bookings/{ref}/cancelIt answers 200 for one of your sandbox bookings.
rate_limit_handledOptionalGo over your key's rate limit (a 429 RATE_LIMITED with your key's X-RateLimit-Remaining: 0), wait Retry-After seconds, retry. A per-IP 429 is answered before the key is read and is not recorded.A request succeeds at least 1 second after a 429.
error_shapeOptionalAny call that fails with a 4xx, for example GET /availability with no experienceYou read error.code from the envelope.

Note

If your account is invoiced to you (on account) instead of paid by the customer on a pay link, there is no pay page, so pay_success and pay_declined are not required for you. Your progress line shows the exact count.

How do I create the practice booking?

Quote the cart first (quote), then book it with the same date, time and items. Save this as booking.json:

jsonbooking.json
{
  "date": "2026-11-14",
  "time": "10:00",
  "items": [
    { "experience": "supercar-drive", "optionId": "opt_5lap" }
  ],
  "customer": { "name": "Test Customer", "email": "test.customer@example.com" },
  "drivers": [{ "name": "Test Driver" }]
}
POST/bookings
curlCreate a sandbox booking
curl -X POST https://<your-booking-domain>/api/partner/v1/bookings \
  -H "Authorization: Bearer pk_test_AbC123dEf456GhI7.xxxxxxxxxxxxxxxx" \
  -H "Idempotency-Key: cert-001" \
  -H "Content-Type: application/json" \
  -d @booking.json

Use a different Idempotency-Key for each new booking. The response carries the bookingRef (for example TST-12) that you pass to the read and cancel calls, and, for pay-link accounts, the payLinkUrl for the two payment steps.

How do I do the two payment steps?

Open the payLinkUrl of an awaiting_payment sandbox booking in a browser and choose Simulate successful payment. That passes pay_success. For pay_declined, open the pay link of another booking and choose Simulate declined card; a booking made with +declined in the customer email always declines. No card is asked for and no money moves. After each one, read the booking again with GET /bookings/{ref} and make sure your system shows the right state.

How do I trigger the failures on purpose?

Add a tag to the local part of the customer email on a POST /bookings with a test key:

  • +soldout answers 409 UNAVAILABLE (sold_out_handled).
  • +duplicate answers 409 DUPLICATE (duplicate_handled).
  • +declined makes the sandbox pay page decline (pay_declined).
jsonA sold-out trigger
"customer": {
  "name": "Test Customer",
  "email": "test.customer+soldout@example.com"
}

Your integration should show the customer a clear message for each one rather than a generic error. The full tag list is on the Sandbox page.

How do I test a safe retry?

curlreplay
# Send the same request twice: same Idempotency-Key, same body.
# First call: 201 Created.  Second call: 200 OK with "replayed": true.
curl -X POST https://<your-booking-domain>/api/partner/v1/bookings \
  -H "Authorization: Bearer pk_test_AbC123dEf456GhI7.xxxxxxxxxxxxxxxx" \
  -H "Idempotency-Key: cert-replay-001" \
  -H "Content-Type: application/json" \
  -d @booking.json

The first call creates the booking (201). The second returns the original booking with replayed: true (200) instead of a second booking. This is what protects you from double bookings when a request times out.

How do I test a cancellation?

POST/bookings/{ref}/cancel
curlcancel
curl -X POST https://<your-booking-domain>/api/partner/v1/bookings/TST-12/cancel \
  -H "Authorization: Bearer pk_test_AbC123dEf456GhI7.xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Certification test" }'

How do I see my progress?

Two places, both with a test key:

  • GET /me includes a certification block: every step, how many required steps have passed, and the next step to do with a short hint.
  • GET /certification returns the same checklist as a walkthrough: the next step plus the exact request to make for it, filled in with one of your own experiences.
GET/me
jsonGET /me with a test key (abridged)
{
  "ok": true,
  "data": {
    "certification": {
      "steps": [
        { "id": "catalog", "title": "List your experiences", "required": true, "passed": true, "passedAt": "2026-10-20T17:02:11.000Z" },
        { "id": "experience", "title": "Fetch one experience", "required": true, "passed": false, "passedAt": null }
      ],
      "requiredPassed": 1,
      "requiredTotal": 14,
      "complete": false,
      "certified": false,
      "nextStep": {
        "id": "experience",
        "title": "Fetch one experience",
        "required": true,
        "hint": "Call GET /experiences/{slug} for one slug from the list."
      }
    },
    "...": "rest of GET /me"
  },
  "requestId": "req_2b9e10"
}

complete: true means every required step has passed. certified: true means the operator has also confirmed it. Live keys do not return this block, and GET /certification with a live key answers 403 FORBIDDEN (details.reason: live_key).

What happens when I have passed?

Tell the operator you are done. They check the checklist, confirm the certification and enable live keys for your account; until then pk_live_ keys answer SANDBOX_ONLY. You do not enable live access yourself. Then follow the steps in How do I go live?.