Skip to content

Bot protection

Creating a hold is the only unauthenticated write that costs us money (an SMS) and blocks a real slot. In production it is protected by Cloudflare Turnstile.

Look at the site payload:

{ "captchaSiteKey": "0x4AAAAAAA…" }
  • Present → attach captchaToken to POST /holds, or you get 400 CAPTCHA_FAILED. Outside local development it is always present, on live and sandbox sites alike: the API refuses to run without Turnstile.
  • Absent → only on a local development API. Send nothing.

A token is accepted only when Cloudflare confirms it and:

  • it was solved for the action hold (pass action: 'hold' to the widget, or data-action="hold");
  • it was solved on one of the studio’s own hosts: its hosted site (<slug>.sites.mrcharles.app) or its verified custom domain. For sandbox sites the official docs host also counts, so the examples here work.

A token solved anywhere else (another domain, another action) gets 400 CAPTCHA_FAILED. If your frontend lives on a domain that is not one of these, use the embedded booking (its iframe runs on the studio’s host) for holds.

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="SITE_KEY_FROM_PAYLOAD" data-action="hold" data-callback="onToken"></div>
let captchaToken = '';
window.onToken = (token) => { captchaToken = token; }; // now the Book button can be enabled
await post(`/public/sites/${slug}/holds`, { ...payload, captchaToken });

Render it explicitly if you prefer:

const id = turnstile.render('#captcha', {
sitekey: site.captchaSiteKey,
action: 'hold',
callback: (t) => setToken(t),
'expired-callback': () => setToken(''),
'error-callback': () => setToken(''),
});
  • A token is single-use. After a failed hold (slot taken, validation error), reset the widget and get a fresh one: turnstile.reset(id).
  • Tokens expire after about five minutes. If the guest fills the form slowly, refresh before submitting.
  • The token is bound to the site key, not to the guest. Do not cache or share it.

Turnstile is a browser widget and holds need it. There is no origin allow-list or captcha bypass for native apps or backend jobs today; open the studio’s hosted booking page or the embed in a web view instead. Do not try to automate the widget.