Forms
POST /_platform/forms takes a form a visitor sent on a site. In one step, Digize stores the submission, finds the CRM contact by the workspace’s duplicate rule (CRM settings) or adds one, adds Filled in the name form to their timeline, and starts every active automation whose Form submitted trigger matches the form.
A static site’s forms can post to /_platform/forms as they are. A site with its own Worker passes the visitor’s post on, with the visitor’s address:
if (url.pathname === "/_platform/forms" && request.method === "POST") { return fetch(new URL("/_platform/forms", url), { method: "POST", headers: { "content-type": request.headers.get("content-type") ?? "", "x-forwarded-for": request.headers.get("cf-connecting-ip") ?? "", referer: request.headers.get("referer") ?? "", }, body: await request.arrayBuffer(), redirect: "manual", });}The first address in x-forwarded-for is the visitor’s, for the rate limit and Turnstile. The referer is kept as the page the form was on.
A plain HTML form
Section titled “A plain HTML form”A urlencoded or multipart post is read field by field. A few names are the platform’s:
| Field | |
|---|---|
_form (or form) |
The form’s name, such as contact or quote. Defaults to contact. Automations and the timeline go by it. |
_redirect |
Where the browser goes next, on the site’s own hostname, such as /thanks. |
cf-turnstile-response |
The Turnstile widget’s token, which it adds to the form. Required. |
utm_source, utm_medium, utm_campaign, utm_term, utm_content |
Where the visitor came from: kept as the contact’s first touch, not as form fields. |
gclid, gbraid, wbraid, fbclid, msclkid, ttclid, li_fat_id |
Ad click ids, kept the same way. |
Any other name starting with _ is dropped, except the _gotcha honeypot, and so are files: forms take text only. Everything else is one of the form’s fields.
A post that goes through answers 303 to _redirect, or else back to the page it came from when that’s on the site, or else the site’s home page. One that doesn’t answers its error as JSON.
<form method="post" action="/_platform/forms"> <input type="hidden" name="_form" value="quote" /> <input type="hidden" name="_redirect" value="/thanks" /> <input name="name" placeholder="Your name" /> <input name="email" type="email" required /> <input name="phone" type="tel" /> <textarea name="details"></textarea> <div class="cf-turnstile" data-sitekey="TURNSTILE_SITE_KEY"></div> <button>Get a quote</button></form><script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>With content-type: application/json, a form is either the same flat fields as an object, or this shape:
{ "form": "quote", "fields": { "name": "Maya Chen", "email": "maya@example.com", "details": "Two bags a month" }, "turnstileToken": "0.ZXhhbXBsZQ…", "pageUrl": "https://brightwave.yourfreesite.com/wholesale", "attribution": { "utm_source": "newsletter" }}Values that aren’t strings are sent as their JSON text. A JSON post always answers JSON, never a redirect:
{ "submissionId": "fs_01JF8Q2M4N6P8R0T2V4X6Z8B0D" }What reaches the contact
Section titled “What reaches the contact”| Field | Becomes |
|---|---|
email, phone |
The contact’s email and phone. At least one is needed, or the form answers 400 invalid with details.fields.email. A phone without a country code takes the CRM’s default country. |
name |
Split into first and last name at the first space. |
first_name or firstName, last_name or lastName |
First and last name, over name. |
city |
The contact’s city. |
tags |
Tags to add, separated by commas. |
| A custom field’s key | Its value, for custom fields with Show on forms on (CRM › Custom fields). |
The first 100 fields are kept on the submission, each value cut to 10,000 characters, and they’re in the automation’s trigger data with the submission’s id, the site, the hostname, the form’s name, the contact’s id, whether the contact is new, and the attribution.
A non-empty _gotcha field, in any of the formats, is a honeypot: the post is thanked and dropped, with nothing stored. Hide it from people (not with type="hidden", which bots skip) and leave it empty.
Checks
Section titled “Checks”In this order, cheapest first:
- The body is at most 256 KB, counted as it streams in:
413 too_large. - At most 10 posts a minute from one visitor to one site:
429 rate_limited. - Turnstile’s token is checked with Cloudflare:
403 forbiddenwhen it doesn’t pass or is missing.
The Turnstile site key
Section titled “The Turnstile site key”Tokens come from Digize’s own Turnstile widget: put its site key in the form’s data-sitekey. GET /_platform/forms answers the key, and whether the widget runs on the hostname the request came to:
{ "turnstileSiteKey": "0x4AAAA…", "turnstileReady": true }The SDK reads it with forms.settings().