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.
Do you need it?
Section titled “Do you need it?”Look at the site payload:
{ "captchaSiteKey": "0x4AAAAAAA…" }- Present → attach
captchaTokentoPOST /holds, or you get400 CAPTCHA_FAILED. - Absent → no captcha configured (typical in pre-production). Send nothing.
Browser
Section titled “Browser”<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(''),});Rules that bite
Section titled “Rules that bite”- 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.
Native apps and server-to-server
Section titled “Native apps and server-to-server”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.