Get a sandbox key
Verify your email and get a test key in about 30 seconds — no signup, no card. The key is locked to our shared demo restaurant, has all four scopes, and expires after 30 days.
After you submit, we email you a link. Opening it takes you to the verification page, where your key is shown once — copy it there and then save it somewhere safe. Building for production instead? Real keys are issued manually — see Getting a key.
First booking in 5 minutes
Four steps, all runnable against our shared demo restaurant
(00000000-0000-0000-0000-000000000010). Copy, paste, get a green response.
-
1. Get a sandbox key
Use the form above — enter your email and system name, open the link we email you, and copy the key shown once on the verification page. No signup, no card. Swap
gbp_your_sandbox_keybelow for your key. -
2. Check availability
Read open times for a party of two (use any upcoming date):
curl "https://api.goboblo.com/v1/partner-api/locations/00000000-0000-0000-0000-000000000010/availability?date=2026-07-20&partySize=2" \ -H "Authorization: Bearer gbp_your_sandbox_key"Response 200:
{ "locationId": "00000000-0000-0000-0000-000000000010", "date": "2026-07-20", "timezone": "Europe/Stockholm", "slots": [ { "slot": "2026-07-20T09:00:00.000Z", "available": true, "remainingCovers": 100 }, { "slot": "2026-07-20T09:15:00.000Z", "available": true, "remainingCovers": 100 } ] }Slot times are UTC. The demo restaurant is open 11:00–22:00
Europe/Stockholm, so the first slot is09:00Zin summer. -
3. Create a booking
Pick a
startsAtfrom an available slot and send anidempotencyKeyso a retry never double-books. Make the key your own unique string — the sandbox restaurant is shared, and a key another developer already used replays their booking instead of creating yours:curl -X POST "https://api.goboblo.com/v1/partner-api/locations/00000000-0000-0000-0000-000000000010/bookings" \ -H "Authorization: Bearer gbp_your_sandbox_key" \ -H "Content-Type: application/json" \ -d '{ "startsAt": "2026-07-20T17:00:00.000Z", "partySize": 2, "name": "Test Guest", "email": "you@example.com", "idempotencyKey": "yourname-first-booking-1" }'Response 201:
{ "id": "b1a2…", "startsAt": "2026-07-20T17:00:00.000Z", "status": "confirmed", "partnerBookingRef": null }Keep the returned
idfor the next step. -
4. Cancel it
Replace
BOOKING_IDwith theidfrom step 3:curl -X POST "https://api.goboblo.com/v1/partner-api/bookings/BOOKING_ID/cancel" \ -H "Authorization: Bearer gbp_your_sandbox_key"Response 200:
{ "ok": true }
That is the full round trip. Every endpoint has curl, JavaScript and PHP examples below, and the sandbox rules are in Sandbox / demo restaurant.
Overview
- Server-to-server. Call us from your backend with a secret key — never from a browser or app.
- You read open times, create bookings, read booking status, cancel, and read the menu for the locations your key may see.
- Goboblo owns the booking: table plan, double-booking protection, guest confirmation email.
Glossary
| Term | Meaning |
|---|---|
locationId | Goboblo's location UUID. You reference a restaurant with this. Issued to you by us. |
API key (gbp_…) | Your secret. Shown in cleartext once when we create it — store it safely. We keep only a hash. |
| Scope | What the key may do (see the table below). |
partnerBookingRef | Your own booking id. If you send it, we store it and mirror it back (cross-reference). |
idempotencyKey | Your retry key on create. Same key + location → the same booking back, never a duplicate. |
| Webhook endpoint | Your URL where we POST booking.*
events, with its own signing secret (whsec_…). |
Authentication
Send the key on every request, one of two ways:
Authorization: Bearer gbp_your_key_here or
x-goboblo-key: gbp_your_key_here - The key is issued by us. Never put it in the frontend.
- Missing or invalid key →
401. Key without the right scope →403.
Scopes
| Scope | Grants access to |
|---|---|
availability:read | GET …/availability |
bookings:read | GET …/bookings/:id |
bookings:write | POST …/bookings,
POST …/bookings/:id/cancel |
menu:read | GET …/menu |
A key can also be locked to specific locations
(allowed_location_ids). Call a location outside the list →
403 (availability/booking) or 404 (status/cancel — we do not
reveal that the booking exists).
Every key is issued with an explicit location list. A key without a list
can, for safety reasons, create bookings but not read or cancel them
(403 "nyckeln saknar lokal-allowlist" on GET /
cancel) — if your key is missing locations you need,
get in touch and we update the list.
Rate limits
- Per key: default
120requests/minute (can be raised per key — ask us). Over the limit →429. - Build in backoff on
429. - The limit is counted per authenticated key — a request with an
invalid key gets
401straight away and never reaches the counter.
Errors
Standard JSON (NestJS). The message string is human-readable and may be
localized — branch on statusCode:
{ "statusCode": 403, "message": "saknar scope: bookings:write", "error": "Forbidden" } | Code | Means |
|---|---|
400 | Invalid input (e.g. wrong date format, invalid
partySize). |
401 | Missing or invalid key. |
403 | Key lacks the scope, or may not see the location. |
404 | Booking or location not found (or not visible to the key). |
409 | Conflict — the time/table was just taken (concurrency protection), or the location is closed. |
429 | Per-key rate limit exceeded. |
503 | The Partner API is not enabled yet (operational state) — try again later. |
Endpoints
1. Availability
GET /v1/partner-api/locations/{locationId}/availability?date=YYYY-MM-DD&partySize=N Scope: availability:read. Designed for sub-1-second responses. Micro-cached
about 8 seconds per locationId|date|partySize.
Response 200:
{
"locationId": "8f3c…",
"date": "2026-07-20",
"timezone": "Europe/Stockholm",
"slots": [
{ "slot": "2026-07-20T17:00:00.000Z", "available": true, "remainingCovers": 12 },
{ "slot": "2026-07-20T17:30:00.000Z", "available": false, "remainingCovers": 0 }
]
} If the location is closed that day → slots: [].
Code examples
curl "https://api.goboblo.com/v1/partner-api/locations/00000000-0000-0000-0000-000000000010/availability?date=2026-07-20&partySize=2" \
-H "Authorization: Bearer gbp_your_sandbox_key" const res = await fetch(
"https://api.goboblo.com/v1/partner-api/locations/00000000-0000-0000-0000-000000000010/availability?date=2026-07-20&partySize=2",
{ headers: { Authorization: "Bearer gbp_your_sandbox_key" } }
);
const data = await res.json(); <?php
$ch = curl_init("https://api.goboblo.com/v1/partner-api/locations/00000000-0000-0000-0000-000000000010/availability?date=2026-07-20&partySize=2");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer gbp_your_sandbox_key"]);
$data = json_decode(curl_exec($ch), true); 1b. Availability — batch (date range × party sizes)
POST /v1/partner-api/locations/{locationId}/availability/batch Scope: availability:read (same as the single call). Built for
Reserve with Google-style batch lookups and feed generation: a whole date range × several
party sizes in one call, instead of hundreds of single calls.
Micro-cached about 8 seconds per locationId|startDate|endDate|partySizes;
identical concurrent calls share one fetch (single-flight).
Body:
{
"startDate": "2026-07-20",
"endDate": "2026-08-18",
"partySizes": [2, 4, 6]
} Limits: max 92 days per call (endDate ≥
startDate), 1–12 party sizes, each size 1–50. Outside the
limits → 400. Duplicates in partySizes are merged.
Response 200
Grouped per (day, party size), sorted by day → size → time:
{
"locationId": "8f3c…",
"startDate": "2026-07-20",
"endDate": "2026-08-18",
"timezone": "Europe/Stockholm",
"days": [
{ "date": "2026-07-20", "partySize": 2, "slots": [
{ "slot": "2026-07-20T15:00:00.000Z", "available": true, "remainingCovers": 12 },
{ "slot": "2026-07-20T15:30:00.000Z", "available": false, "remainingCovers": 0 }
] },
{ "date": "2026-07-20", "partySize": 4, "slots": [
{ "slot": "2026-07-20T15:00:00.000Z", "available": true, "remainingCovers": 12 }
] },
{ "date": "2026-07-21", "partySize": 2, "slots": [ … ] }
]
} - Responses are identical to the single call for each (day, size) —
same slots, same
available, sameremainingCovers. You can mix single and batch calls freely. - Days missing from
dayshave no availability: the location is closed that day, or no service period allows that party size. There is never an entry with an emptyslotslist — absence means nothing is bookable. - One entry per
partySizeper day maps cleanly to feed formats that need one availability row per party size and time (e.g. Reserve with Google).
Code examples
curl -X POST "https://api.goboblo.com/v1/partner-api/locations/00000000-0000-0000-0000-000000000010/availability/batch" \
-H "Authorization: Bearer gbp_your_sandbox_key" \
-H "Content-Type: application/json" \
-d '{ "startDate": "2026-07-20", "endDate": "2026-07-27", "partySizes": [2, 4] }' const res = await fetch(
"https://api.goboblo.com/v1/partner-api/locations/00000000-0000-0000-0000-000000000010/availability/batch",
{
method: "POST",
headers: {
Authorization: "Bearer gbp_your_sandbox_key",
"Content-Type": "application/json",
},
body: JSON.stringify({ startDate: "2026-07-20", endDate: "2026-07-27", partySizes: [2, 4] }),
}
);
const data = await res.json(); <?php
$ch = curl_init("https://api.goboblo.com/v1/partner-api/locations/00000000-0000-0000-0000-000000000010/availability/batch");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer gbp_your_sandbox_key",
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"startDate" => "2026-07-20", "endDate" => "2026-07-27", "partySizes" => [2, 4],
]));
$data = json_decode(curl_exec($ch), true); 2. Create booking
POST /v1/partner-api/locations/{locationId}/bookings Scope: bookings:write.
Body:
{
"startsAt": "2026-07-20T17:00:00.000Z",
"partySize": 4,
"name": "Anna Andersson",
"email": "anna@example.com",
"phone": "+46701234567",
"notes": "Window table if possible",
"partnerBookingRef": "rwg-abc-123",
"idempotencyKey": "your-unique-retry-key-123",
"lang": "sv"
} phone, notes, partnerBookingRef,
idempotencyKey and lang are optional. lang ∈
sv|en|no|da (sets the guest's confirmation/reminder language).
Response 201:
{ "id": "b1a2…", "startsAt": "2026-07-20T17:00:00.000Z", "status": "confirmed", "partnerBookingRef": "rwg-abc-123" } - Goes through the exact same booking engine as the guest widget
(advisory lock + GiST) → concurrency-safe. Two concurrent bookings for the last table →
exactly one
201, one409. - The booking gets
source = "partner"andchannel = "partner:<your-slug>"(visible in the restaurant's channel stats). The guest gets a confirmation email in their language as usual. - We leak no manage token to you; the guest self-serves via the email link. You cancel via the booking id below.
Idempotency (idempotencyKey) — protection against double booking on retry
Send your own unique key (max 120 characters, e.g. a UUID or Google's
idempotency_token) in idempotencyKey. Then:
- Replay: the same key against the same location again → you get the same booking back (200 content in a 201 response, the exact same shape) — a duplicate is never created. This holds even if two retries arrive at the same time: exactly one booking is created, both calls get it.
- Got a timeout or network error on a create? Resend the exact same request with the same key — that is the whole point. Change the key only when you deliberately want to create a NEW booking.
- The key is unique per location — different locations can use the same key without conflict.
idempotencyKey≠partnerBookingRef: the ref is a soft cross-reference for tracking (may be reused, mirrored in status/webhooks); the key is a hard uniqueness guarantee that governs the create flow itself.- Leave the key out and everything works as before — but then a retry is a new booking. Strongly recommended for anyone building automatic retry.
Replay example (second call with the same key):
{ "id": "b1a2…", "startsAt": "2026-07-20T17:00:00.000Z", "status": "confirmed", "partnerBookingRef": "rwg-abc-123" } Same id as the first response. If the booking has since changed,
status reflects it (e.g. cancelled).
Code examples
curl -X POST "https://api.goboblo.com/v1/partner-api/locations/00000000-0000-0000-0000-000000000010/bookings" \
-H "Authorization: Bearer gbp_your_sandbox_key" \
-H "Content-Type: application/json" \
-d '{
"startsAt": "2026-07-20T17:00:00.000Z",
"partySize": 2,
"name": "Test Guest",
"email": "you@example.com",
"idempotencyKey": "yourname-unique-key-1"
}' const res = await fetch(
"https://api.goboblo.com/v1/partner-api/locations/00000000-0000-0000-0000-000000000010/bookings",
{
method: "POST",
headers: {
Authorization: "Bearer gbp_your_sandbox_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
startsAt: "2026-07-20T17:00:00.000Z",
partySize: 2,
name: "Test Guest",
email: "you@example.com",
idempotencyKey: "yourname-unique-key-1",
}),
}
);
const booking = await res.json(); <?php
$ch = curl_init("https://api.goboblo.com/v1/partner-api/locations/00000000-0000-0000-0000-000000000010/bookings");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer gbp_your_sandbox_key",
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"startsAt" => "2026-07-20T17:00:00.000Z",
"partySize" => 2,
"name" => "Test Guest",
"email" => "you@example.com",
"idempotencyKey" => "yourname-unique-key-1",
]));
$booking = json_decode(curl_exec($ch), true); 3. Read booking status
GET /v1/partner-api/bookings/{bookingId} Scope: bookings:read.
Response 200:
{
"id": "b1a2…",
"locationId": "8f3c…",
"startsAt": "2026-07-20T17:00:00.000Z",
"partySize": 4,
"status": "confirmed",
"source": "partner",
"channel": "partner:reserve-with-google",
"partnerBookingRef": "rwg-abc-123",
"createdAt": "2026-07-09T10:00:00.000Z"
} No extra guest PII beyond what you sent in yourself (see GDPR below).
Code examples
curl "https://api.goboblo.com/v1/partner-api/bookings/BOOKING_ID" \
-H "Authorization: Bearer gbp_your_sandbox_key" const res = await fetch(
"https://api.goboblo.com/v1/partner-api/bookings/BOOKING_ID",
{ headers: { Authorization: "Bearer gbp_your_sandbox_key" } }
);
const booking = await res.json(); <?php
$ch = curl_init("https://api.goboblo.com/v1/partner-api/bookings/BOOKING_ID");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer gbp_your_sandbox_key"]);
$booking = json_decode(curl_exec($ch), true); 4. Cancel
POST /v1/partner-api/bookings/{bookingId}/cancel Scope: bookings:write. Idempotent — already cancelled →
{ "ok": true }.
Response 200:
{ "ok": true } Bookings in a final state that cannot be cancelled (e.g. already completed or marked
no-show) → 400 "bokningen går inte att avboka".
Code examples
curl -X POST "https://api.goboblo.com/v1/partner-api/bookings/BOOKING_ID/cancel" \
-H "Authorization: Bearer gbp_your_sandbox_key" const res = await fetch(
"https://api.goboblo.com/v1/partner-api/bookings/BOOKING_ID/cancel",
{ method: "POST", headers: { Authorization: "Bearer gbp_your_sandbox_key" } }
);
const data = await res.json(); // { ok: true } <?php
$ch = curl_init("https://api.goboblo.com/v1/partner-api/bookings/BOOKING_ID/cancel");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer gbp_your_sandbox_key"]);
$data = json_decode(curl_exec($ch), true); // ["ok" => true] Sandbox / demo restaurant
Every sandbox key is locked to one shared demo restaurant, so you can build and test without touching a real venue. It has all four scopes, a rate limit of 60 requests/minute, and expires after 30 days. You can hold up to three active sandbox keys per email.
- Location id
00000000-0000-0000-0000-000000000010- Timezone
Europe/Stockholm- Open
- Every day 11:00–22:00 (responses are UTC)
- Slots
- Every 15 minutes
Tables at the demo restaurant:
| Table | Seats | Note |
|---|---|---|
T1 | 1–2 | — |
T2 | 2–4 | — |
T3 | 4–6 | Window — the only table that seats 6 |
T4 | 1–2 | Bar |
Because the demo restaurant is shared, testers compete for the same tables — book a party
of six on the same slot twice to see the 409 concurrency guard in action.
Auto-cleanup. Bookings on the demo restaurant older than 24 hours are cancelled nightly, so the calendar stays open every morning. Sandbox keys expire after 30 days — request a new one with the same email to keep going.
Booking sources / attribution
Every partner booking is marked with source = "partner" and
channel = "partner:<slug>" (e.g.
partner:reserve-with-google). If you send
partnerBookingRef, it is mirrored back in the status response and in the
webhook payload.
Webhooks (outgoing events to you)
If you want to know when bookings change, we register a webhook endpoint
for you (URL + a signing secret whsec_… shown once). We then POST JSON to your
URL.
Event types
| We send | When |
|---|---|
booking.created | A booking was created. |
booking.updated | A booking was moved or changed. |
booking.cancelled | A booking was cancelled. |
Your endpoint can subscribe to a subset, and can be locked to specific locations.
Payload
{
"id": "b1a2…:booking.created:1752000000",
"type": "booking.created",
"created": 1752000000,
"data": {
"bookingId": "b1a2…",
"locationId": "8f3c…",
"status": "confirmed",
"startsAt": "2026-07-20T17:00:00.000Z",
"endsAt": "2026-07-20T19:00:00.000Z",
"partySize": 4,
"source": "partner",
"channel": "partner:reserve-with-google",
"partnerBookingRef": "rwg-abc-123",
"guest": { "name": "Anna Andersson", "email": "anna@example.com", "phone": "+46701234567" }
}
} Headers
| Header | Content |
|---|---|
x-goboblo-signature | t=<unix>,v1=<hex>
— see signature verification below. |
x-goboblo-event-id | Same as id in the payload —
use as the idempotency key. |
content-type | application/json |
Signature verification (Stripe-style)
Sign the raw body (not a re-serialized version) together with the
timestamp: v1 = HMAC_SHA256(secret, "<t>.<rawBody>") in hex, where
secret is your endpoint's whsec_….
Node.js example:
const crypto = require("crypto");
function verify(rawBody, header, secret) {
// header = "t=1752000000,v1=abcd…"
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
// Constant-time comparison:
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false;
// REQUIRED replay protection: reject deliveries older than 5 minutes — without
// the tolerance check, an intercepted delivery can be replayed with a valid signature.
return Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
} Respond 2xx quickly (within about 5 s). Any other response or a timeout
counts as a failure — we retry the delivery.
Replay protection, summarized: (1) verify the signature on the raw
body, (2) reject |now − t| > 300 s, (3) de-duplicate on
x-goboblo-event-id (the same event may be delivered more than once — the
second time should be a no-op with 2xx).
Retry + idempotency
- On failure we make about 5 attempts with growing delay (10 s, 1 min, 5 min, 30 min).
- The same event carries the same
x-goboblo-event-idon every attempt — de-duplicate on it. - If your endpoint keeps failing (about 15 consecutive failed events) we disable it; we re-enable it manually once it works again.
GDPR / roles
The restaurant is the data controller for guest data. Goboblo is a data
processor; you as a partner are a channel/processor. The guest block in the
webhook is opt-in per endpoint (off by default — then
"guest": null in the payload) and is turned on only when a processing
agreement exists and you need the guest data to mirror the booking (e.g. Reserve with
Google sync) — handle it accordingly and delete it per your agreement.
Test checklist
- Request without / with a wrong key →
401; a valid key without the right scope →403. - Key locked to locations →
403/404against a location outside the list. GET availabilityresponds in under 1 s and matches the widget.POST availability/batchgives the same slots as the single calls for each (day, size); 93 days or 13 sizes →400.POST bookingscreates a booking visible to the restaurant with channelpartner:<slug>.GET/cancelon a booking your key did NOT create (another channel) →404— you only see your own.- A webhook delivery older than 5 min is rejected by your verification; a duplicated
x-goboblo-event-idis a no-op. - Two concurrent bookings for the last table → exactly one
201, one409. POST bookingstwice with the sameidempotencyKey→ the same booking id both times, only ONE booking at the restaurant.GET bookings/:idmirrorspartnerBookingRefand the right status.POST cancelis idempotent (second call →{ "ok": true }).GET menureturns the menu in the format above.- You can recompute
v1with your secret over"<t>.<rawBody>"and get the same hex. - You de-duplicate on
x-goboblo-event-idon retry. - On
429: you back off and try again.
OpenAPI spec
A machine-readable OpenAPI 3 description of every endpoint, request, response and error is published alongside this page. Load it into Postman, Insomnia or your own code generator.
Download the OpenAPI spec (YAML)
The spec is kept in sync with this reference and the underlying v1 contract.
Changelog & versioning
| Date | Version | Change |
|---|---|---|
| 2026-07 | v1 | Initial release: availability (single + batch), create/read/cancel bookings, read menu, and signed webhooks. |
Versioning policy
v1is stable. Additive changes — new optional fields or new endpoints — ship without a version bump, so build defensively and ignore fields you do not use.- Breaking changes ship as a new version (
/v2);v1stays supported and running in parallel, with notice before anything is retired.
Support
Questions or something not working? Email help@goboblo.com — we respond within one business day.
Getting a key
Sandbox: self-serve in about 30 seconds with the form above — a test key locked to the demo restaurant. Manage your keys any time in the developer portal.
Production: real keys are issued manually by us, one per partner, with an explicit location list (this keeps the data-processing agreement and per-webhook PII opt-in in place). Apply for production access and we respond within one business day: start on Build with Goboblo, email help@goboblo.com, or get in touch — tell us what your system should do and we set up the key and any webhook endpoint together with you.