Skip to content

Holds

A hold blocks a slot while the guest proves they are reachable. It lives 12 minutes; the code inside it lives 10.

Terminal window
curl -s -X POST https://api.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": "phone",
"contact": "+34600111222",
"locale": "es",
"idempotencyKey": "6f1c0e3a-6b0e-4f0c-9f1a-2a8f1b7c4d55",
"captchaToken": "0.AbC…"
}'
FieldTypeRequiredNotes
roomIduuid✅Must be bookable
datestring✅YYYY-MM-DD, studio time zone
startTimestring✅HH:MM, on the stepMinutes grid
durationMinutesnumber✅Between minMinutes and maxMinutes, multiple of stepMinutes
guestNamestring✅2–120 characters
channelstring✅phone or email
contactstring✅E.164 phone (+34600111222) or an email address
localestring—es (default) · ca · en — language of the code message
idempotencyKeystring✅One per attempt; reuse it when retrying the same request
captchaTokenstringconditionalRequired when the site payload has captchaSiteKey

No price is sent. The server computes it.

{
"holdId": "b59b9109-1beb-44fa-a679-c2d612844c62",
"status": "pending",
"roomId": "d9a36c09-…",
"roomName": "Rehearsal B",
"date": "2026-10-08",
"startTime": "17:00",
"endTime": "19:00",
"timezone": "Europe/Madrid",
"price": { "priceCents": 5000, "depositCents": 1500, "currency": "EUR", "minutes": 120 },
"channel": "phone",
"contactMasked": "+34 ••• ••• 222",
"expiresAt": "2026-10-06T12:26:23+02:00",
"resendsLeft": 2,
"paymentModes": ["card", "on_site"],
"devCode": "181989"
}

price.depositCents is what a card payment charges now; priceCents is the full price. contactMasked is safe to display. devCode exists only in pre-production without a delivery provider — never in production.

Same key + same payload → the same hold (no second SMS). Same key + different payload → 400 VALIDATION_ERROR. After a hold expires, use a new key.

StatusCodeCause
400VALIDATION_ERRORBad duration, malformed contact, idempotency mismatch
400CAPTCHA_FAILEDMissing, reused or expired captcha token
404SITE_NOT_LIVESite unpublished
409ROOM_NOT_BOOKABLEOnline booking off for this room
409OUTSIDE_HOURSThe room is closed at that time
409OUTSIDE_BOOKING_WINDOWToo soon (minNoticeMin) or too far (maxAdvanceDays)
409SLOT_TAKENSomeone else got there first
429RATE_LIMIT_EXCEEDED15/min per IP · 5/h per contact · 20/h per IP
503SMS_UNAVAILABLESMS delivery is down — offer channel: "email"

{ "code": "181989" }

Returns the same hold shape with "status": "verified" and a refreshed expiresAt — the guest now has another 12 minutes to choose how to pay.

StatusCodeCause
400CODE_INVALIDWrong code, or older than 10 minutes
429TOO_MANY_ATTEMPTS5 wrong codes — the hold is burnt, start over
410HOLD_EXPIREDThe 12 minutes ran out

No body. Sends a fresh code and resets the attempt counter.

  • Maximum 3 sends per hold (the first one included) — resendsLeft tells you how many are left.
  • At least 60 seconds between sends, otherwise 429 RATE_LIMIT_EXCEEDED.
POST /holds verify confirm
(none) ─────────────────▶ pending ──────────▶ verified ──────────▶ confirmed
│ │
5 wrong codes ────┤ ├──── 12 min ────▶ expired
12 min ─────┘

An expired or burnt hold frees the slot immediately. There is no “un-expire”: create a new hold.