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.
  • Absent → no captcha configured (typical in pre-production). Send nothing.
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="SITE_KEY_FROM_PAYLOAD" 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,
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. If you are integrating from a native app or a backend job, contact us — we can allow-list an origin or agree on another mechanism. Do not try to automate the widget.