Post leads from your own form
The public capture endpoint — send a lead to Zyan from a form you already built, with a capture token you generate and rotate yourself.
If you already have a contact form you like, you do not have to replace it. The
capture API takes a JSON POST and creates a lead in exactly the same place
a Zyan form would.
Before you start
Generating a capture token is owner-gated. You will find it under
LeadsLead ManagerLead settingsIntegration. A
member with leads edit and settings edit can read that panel's status,
but only an owner can mint, rotate or disable the token.
Two credentials, not one
This is the part that catches people, so it is worth settling before the snippet. Every call carries both of these:
| Credential | Where it goes | What it is |
|---|---|---|
| Workspace publishable key | The apikey and Authorization: Bearer headers | Your workspace's public API key. Safe to put in a page a visitor can read |
| Capture token | The capture_token field in the JSON body | The secret that decides which workspace the lead lands in. Never ship this to a browser |
Miss the headers and the request is refused at the gateway before the token is read at all — which looks exactly like a bad token and is not one. Both values are already filled in on the snippet the Integration panel gives you.
Get a capture token
Open the Integration panel
LeadsLead ManagerLead settingsIntegration. The status pill reads Configured, Not configured, Checking… or Status unavailable.
Generate the token
It looks like <workspace id>.<secret>.
Copy it immediately
The secret half is shown exactly once. If you lose it, rotate to get a new one — you cannot read the old one back.
A capture token is a credential
Anyone holding it can create leads in your workspace. Keep it server-side. If your form posts from the browser, put a small endpoint of your own in front so the token never ships to a visitor.
Send a lead
curl -X POST 'https://<your functions host>/functions/v1/public-lead-capture' \
-H 'Content-Type: application/json' \
-H 'apikey: <workspace publishable key>' \
-H 'Authorization: Bearer <workspace publishable key>' \
-d '{
"capture_token": "<workspace id>.<secret>",
"name": "Dana Reyes",
"email": "dana@example.com",
"subject": "Website redesign",
"message": "We are replacing a 2019 Squarespace site. Budget is flexible."
}'
The exact endpoint URL, your publishable key and a ready-made snippet are all on the Integration panel — copy them from there rather than assembling the host by hand.
Request
Four content fields are required, plus the token. The rest are optional and worth sending — a detail you leave out here is a detail somebody has to ring up and ask for.
| Field | Required | Notes |
|---|---|---|
capture_token | Yes | The full <workspace id>.<secret> string |
name | Yes | |
email | Yes | Lower-cased on the way in, so casing never splits a match |
subject | Yes | |
message | Yes | |
phone | No | |
company | No | |
website | No | |
form_source | No | Your own label for the page that captured it. Defaults to website_contact |
attribution | No | An object for click ids and campaign details. A campaign is honoured only when it belongs to your workspace |
Response
{ "success": true, "lead_id": "…", "message": "…" }
| Status | Meaning |
|---|---|
200 | The lead was created |
400 | A required field is missing, or the message tripped the spam filter |
404 | Anything token-shaped went wrong |
413 | The body is over 32 KB |
429 | The workspace is over 100 captured leads in the last hour |
A 404 never tells you which problem it is
No token minted, a wrong token, a rotated token, a disabled endpoint, an
inactive workspace — every one of them returns the same 404 with the same
body. That is deliberate: nobody probing the endpoint should be able to learn
whether a workspace exists. Diagnose it from the Integration panel, not
from the response.
Caps
| Limit | Value |
|---|---|
| Captured leads | 100 per hour, per workspace |
| Request body | 32 KB |
Testing from one machine will not trip the hourly cap quickly, but a broken loop on your own site will. The cap is counted per workspace, not per form.
Rotating and disabling
Both live on the same Integration panel.
- Rotate issues a new token and kills the old one instantly — update every caller first, or you will drop leads between the two steps.
- Disable turns the endpoint off entirely. Existing leads are untouched.
What these leads look like afterwards
Leads from this endpoint are labelled Custom integration in the operator notification, and their Attribution tab carries a source but no Form — the endpoint has no way to know which of your pages the visitor was on.
Form-scoped automations will never match them
Only hosted, embed and provider submissions carry a form id. If you scope an automation trigger to a specific form, capture-API leads slide straight past it. Use an unscoped lead created trigger for this route.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
400 on every request | A required field is missing | name, email, subject and message are all required — subject is the one people forget |
| Requests stopped working suddenly | The token was rotated | Copy the new token from Integration |
A steady 404 and the panel reads Not configured | No token has been minted, or the endpoint was disabled | Generate a token, or re-enable it on the Integration panel |
| The token is not visible | It is only shown once, at generation | Rotate to mint a fresh one |
| Requests fail before the token is even checked | The publishable key headers are missing | Send it as both apikey and Authorization: Bearer |
429 Too many requests | Over 100 captured leads in the hour | Wait for the hour to roll over, and check nothing is looping |
413 on a large request | The body is over 32 KB | Trim the payload |
A 400 on a request that looks complete | The message tripped the spam keyword filter | Reword it, or capture through a hosted form |
| A form-scoped automation never fires | These leads carry no form id | Use an unscoped lead created trigger |