Quickstart
Four calls take a guest from “when is the room free?” to a confirmed booking. Everything below runs against pre-production, where verification codes come back in the response so you can script the whole flow.
-
Read the site and pick a room
Terminal window curl -s https://api-pre.mrcharles.app/api/v1/public/sites/studio-uno-madrid{"success": true,"data": {"studio": { "name": "Studio Uno", "slug": "studio-uno-madrid", "timezone": "Europe/Madrid", "city": "Madrid" },"rooms": [{"id": "d9a36c09-d151-5d4a-a077-296293b07be4","name": "Rehearsal B","hourlyCents": 2500, "currency": "EUR","bookable": true,"minMinutes": 60, "maxMinutes": 240, "stepMinutes": 60,"paymentModes": ["on_site"], "depositPercent": 100, "cancelNoticeHours": 24}],"bookingOnline": true,"cardPayments": true}}Only rooms with
"bookable": trueaccept online bookings. Full shape: Site payload. -
Ask when it is free
Terminal window curl -s "https://api-pre.mrcharles.app/api/v1/public/sites/studio-uno-madrid/rooms/d9a36c09-d151-5d4a-a077-296293b07be4/availability?from=2026-10-08&to=2026-10-10"{"success": true,"data": {"roomId": "d9a36c09-d151-5d4a-a077-296293b07be4","timezone": "Europe/Madrid","hourlyCents": 2500, "currency": "EUR","minMinutes": 60, "maxMinutes": 240, "stepMinutes": 60,"minNoticeMin": 60, "maxAdvanceDays": 90,"days": [{ "date": "2026-10-08", "free": [{ "start": "09:00", "end": "22:00" }] },{ "date": "2026-10-09", "free": [{ "start": "09:00", "end": "23:00" }] }]}}freeholds continuous open spans in the studio’s time zone, already excluding other bookings, live holds, buffers, the minimum notice and the horizon. Slice them into start times withstepMinutes. See Availability. -
Hold the slot and send the code
Terminal window curl -s -X POST https://api-pre.mrcharles.app/api/v1/public/sites/studio-uno-madrid/holds \-H 'Content-Type: application/json' \-d '{"roomId": "d9a36c09-d151-5d4a-a077-296293b07be4","date": "2026-10-08","startTime": "17:00","durationMinutes": 120,"guestName": "Marta Ruiz","channel": "email","contact": "marta@example.com","locale": "es","idempotencyKey": "6f1c0e3a-6b0e-4f0c-9f1a-2a8f1b7c4d55"}'{"success": true,"data": {"holdId": "b59b9109-1beb-44fa-a679-c2d612844c62","status": "pending","roomName": "Rehearsal B","date": "2026-10-08", "startTime": "17:00", "endTime": "19:00","timezone": "Europe/Madrid","price": { "priceCents": 5000, "depositCents": 5000, "currency": "EUR", "minutes": 120 },"channel": "email","contactMasked": "m••••@example.com","expiresAt": "2026-10-06T12:26:23+02:00","resendsLeft": 2,"paymentModes": ["on_site"],"devCode": "181989"}}The slot is now yours for 12 minutes. The price came back from the server — never compute it yourself.
devCodeappears only in pre-production without a delivery provider; in production the code is sent by SMS or email. -
Verify the code, then confirm
Terminal window curl -s -X POST https://api-pre.mrcharles.app/api/v1/public/holds/b59b9109-.../verify \-H 'Content-Type: application/json' -d '{"code":"181989"}'curl -s -X POST https://api-pre.mrcharles.app/api/v1/public/holds/b59b9109-.../confirm \-H 'Content-Type: application/json' \-d '{"paymentMode":"on_site","notes":"Trio, we bring our own cymbals.","email":"marta@example.com"}'{"success": true,"data": {"booking": {"id": "96f52eb7-3360-486a-8d04-47b1663c049d","reference": "MC-TGRMY5","status": "confirmed","paymentMode": "on_site", "paymentStatus": "unpaid","studioName": "Studio Uno", "roomName": "Rehearsal B","date": "2026-10-08", "startTime": "17:00", "endTime": "19:00","startsAt": "2026-10-08T17:00:00+02:00","priceCents": 5000, "currency": "EUR","cancelNoticeHours": 24, "mayCancel": true},"manageToken": "1000bb60…47da3a0","manageUrl": "https://studio-uno-madrid.sites.mrcharles.app/r/1000bb60…"}}Done. Show the reference, and give the guest the
manageUrl— it is the only way back to the booking.
The same flow in JavaScript
Section titled “The same flow in JavaScript”const API = 'https://api-pre.mrcharles.app/api/v1';const SLUG = 'studio-uno-madrid';
const json = async (res) => { const body = await res.json(); if (!res.ok) throw Object.assign(new Error(body.error?.message ?? res.statusText), { code: body.error?.code }); return body.data;};
// 1 · availability for the next two weeksconst from = new Date().toISOString().slice(0, 10);const to = new Date(Date.now() + 13 * 864e5).toISOString().slice(0, 10);const avail = await fetch(`${API}/public/sites/${SLUG}/rooms/${roomId}/availability?from=${from}&to=${to}`).then(json);
// 2 · hold (one idempotency key per attempt — reuse it when retrying)const idempotencyKey = crypto.randomUUID();const hold = await fetch(`${API}/public/sites/${SLUG}/holds`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ roomId, date: '2026-10-08', startTime: '17:00', durationMinutes: 120, guestName, channel: 'phone', contact: '+34600111222', locale: 'es', idempotencyKey, captchaToken, }),}).then(json);
// 3 · verify the 6-digit code the guest receivedawait fetch(`${API}/public/holds/${hold.holdId}/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }),}).then(json);
// 4 · confirmconst { booking, manageUrl } = await fetch(`${API}/public/holds/${hold.holdId}/confirm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paymentMode: 'on_site', notes, email }),}).then(json);// Calling from your server keeps the guest's IP out of the rate-limit bucket// of your whole site: forward it explicitly.const res = await fetch(`${API}/public/sites/${SLUG}/holds`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Forwarded-For': guestIp, // the real visitor, not your server }, body: JSON.stringify(payload),});Rate limits are counted per client IP. If every call arrives from one server address, your busiest hour will hit the limit for everyone. See Errors & rate limits.
- Choose your integration — hosted, embedded or custom.
- Build your own booking UI — the complete flow with error handling.
- Errors & rate limits — what each code means and what to show.