Forms are built in the admin (Site → Forms). Each has a slug and questions, and each question an id. The form's Integration tab shows its endpoint, its write key and a working snippet.
POST https://intake.foundationcms.org/v1/<site>/forms/<slug> works from day one
POST https://intake.<your domain>/forms/<slug> once the intake address is set up (no CORS preflight)
Content-Type: application/json
x-foundation-key: <the form's write key>
{ "q_name": "Amara Reid", "q_email": "amara@example.com", "q_message": "…",
"cf-turnstile-response": "<token>" }
Answers are keyed by question id. A question the form does not ask is refused, so post only the form's own ids.
When the site has a check in force (Settings → Site → Spam protection: Cloudflare Turnstile by default, or Google reCAPTCHA), draw its widget inside your form with the site key shown there, and send the token it writes — cf-turnstile-response or g-recaptcha-response — with the answers. Wait for the token before submitting: a form sent before the check finishes is refused.
Foundation's own sites post the form to their own Worker, which adds the write key and the visitor's address and forwards it, so the key never reaches the browser. A key in a page's source still only lets someone submit that one form, and the limits below still apply — but server-side is better.
| Status | Means |
|---|---|
| 201 | { "ok": true } — stored, and sent wherever the form delivers. Also the answer to a sender the site has marked as spam: their submission is dropped, deliberately without saying so. |
| 403 | human-check (no token, or it failed), not-authorised (the key does not match this site and form), form-closed (switched off in the admin). |
| 404 | No form with that slug on this site. |
| 413 | The submission is too large. |
| 422 | { "problems": { "<question id>": "<kind>" } } — put each message beside its field. |
| 429 | Too many: 20 a minute per form, 60 a minute per site, 30 an hour from one address. |