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
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).
| Step | Required | The call | Passes when |
|---|---|---|---|
catalog | Yes | GET /experiences | It answers 200. |
experience | Yes | GET /experiences/{slug} | It answers 200 for one slug from the list. |
availability | Yes | GET /availability?experience={slug}&from={date}&to={date} | It answers 200. |
slots | Yes | GET /availability/slots?experience={slug}&date={date} | It answers 200. |
quote | Yes | POST /quote | It answers 200. |
booking_created | Yes | POST /bookings with an Idempotency-Key | It answers 201 and returns a TST- reference. |
booking_fetched | Yes | GET /bookings/{ref} | It answers 200 for one of your bookings. |
booking_listed | Yes | GET /bookings | It answers 200. |
pay_success | Yes, if you settle by pay link | Open a booking's payLinkUrl in a browser and choose Simulate successful payment | The sandbox pay page records the payment. |
pay_declined | Yes, if you settle by pay link | Open 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_handled | Yes | POST /bookings with +soldout in the customer email | It answers 409 UNAVAILABLE. |
duplicate_handled | Yes | POST /bookings with +duplicate in the customer email | It answers 409 DUPLICATE. |
replay | Yes | Send the same POST /bookings twice with the same Idempotency-Key and body | The second call answers 200 with replayed: true. |
cancel | Yes | POST /bookings/{ref}/cancel | It answers 200 for one of your sandbox bookings. |
rate_limit_handled | Optional | Go 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_shape | Optional | Any call that fails with a 4xx, for example GET /availability with no experience | You read error.code from the envelope. |
Note
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:
{
"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" }]
}/bookingscurl -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.jsonUse 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:
+soldoutanswers409 UNAVAILABLE(sold_out_handled).+duplicateanswers409 DUPLICATE(duplicate_handled).+declinedmakes the sandbox pay page decline (pay_declined).
"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?
# 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.jsonThe 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?
/bookings/{ref}/cancelcurl -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 /meincludes acertificationblock: every step, how many required steps have passed, and the next step to do with a short hint.GET /certificationreturns 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.
/me{
"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?.