Lead channel webhooks
POST JSON payloads to create leads programmatically from any external system.
Overview
Each lead channel has its own webhook URL. Posting JSON to that URL creates a new lead under the channel. Use webhooks when the source system can't embed our public form — for example Zapier, Make, n8n, or a custom backend.
Endpoint
Content-Typemust beapplication/json.- The path
{secret}is the channel's webhook secret. - The optional
?channel={slug}query routes the lead to a sibling channel; see Routing across channels. - Rate limit: 30 requests per minute per IP. Excess requests get an HTTP 429.
Authentication
The webhook secret embedded in the URL is the credential. No additional auth header is required.
- Treat the secret like a password. Anyone with the URL can post leads.
- Rotate it from the channel's Webhook tab via Regenerate secret. Existing integrations break the moment you regenerate.
Request payload
The body is a flat JSON object. The intake pipeline extracts four target fields — customerName, customerEmail, customerPhone, message— and stores every other key on the lead's fields map.
Default keys (no custom mapping)
When the channel has no custom mapping, the first non-empty string found at any of these keys is used:
| Target field | Source keys checked (in order) |
|---|---|
| customerName | customerName, customer_name, name, full_name, fullName |
| customerEmail | customerEmail, customer_email, email, e_mail |
| customerPhone | customerPhone, customer_phone, phone, telephone, tel |
| message | message, description, notes, comment, body |
Custom mapping
If the channel has a custom mapping configured (Fields tab), source keys are read directly from your mapping and the default key fallbacks are skipped. Unmapped keys still land in the lead's fields map.
MISSING_NAME and no lead is created.Routing across channels
Every channel has its own webhook secret and its own URL slug. By default, posting to a channel's URL creates a lead in that same channel. The ?channel=<slug> query lets you use one channel's secret to file leads into a different channel in the same tenant.
Use this when the upstream system only has room for one webhook URL but you want to split incoming leads across multiple channels based on something it sends.
general and vip. Both have webhook intake enabled. You only configured one webhook in Zapier, using general's secret.- POST to
…/leads/webhook/<general-secret>→ lead lands ingeneral. - POST to
…/leads/webhook/<general-secret>?channel=vip→ lead lands invip.
- The slugfor each channel is shown on its Webhook tab right below the URL. It's the same slug used by the channel's public form at
/forms/<slug>. - The target channel (the one named in
?channel=) must have webhook intake enabled. The channel that owns the secret does not have to — the secret is purely an auth token in this case. - Without
?channel=, the secret's own channel is the target and it must have webhook intake enabled.
Response
The endpoint always returns HTTP 200 with the body { "received": true }. Errors and skipped requests also return 200; the outcome is recorded as a WebhookEvent row that you can inspect in the channel's Webhook tab under Recent payloads.
Event statuses
Every request produces exactly one WebhookEvent row with one of these statuses:
| Status | Meaning | How to fix |
|---|---|---|
| ACCEPTED | Lead was created from the payload. | No action needed. |
| DUPLICATE | A lead with the same email or phone was already received in the last 5 minutes. Payloads with neither field always bypass this check. | This is a short-window guard against retries and double-submits. Wait a few minutes if you intentionally need to resend. |
| LIMIT_REACHED | Your tenant has hit the monthly lead limit for its plan. | Upgrade your plan or wait until the next billing cycle. |
| MISSING_NAME | The payload didn't contain a value that mapped to customerName. | Send a customerName field, use one of the default keys, or configure a custom mapping on the Fields tab. |
| SECRET_INVALID | The URL secret didn't match any channel. | Copy the current secret from the channel's Webhook tab; it may have been regenerated. |
| CHANNEL_DISABLED | The channel that owns the secret is disabled. | Re-enable the channel from Settings → Leads. |
| WEBHOOK_DISABLED | The channel has webhook intake turned off and the request did not include a ?channel= override. | Toggle "Webhook" on in the channel's Webhook tab, or pass ?channel=<slug> to route to a sibling that already has webhook intake enabled. |
| ROUTED_CHANNEL_NOT_FOUND | ?channel=<slug> did not match a sibling channel with webhook intake enabled. | Check the slug matches an existing sibling channel and that its webhook is enabled. |
| ERROR | Unexpected error while processing the payload. | Retry. If it persists, contact support with the payload from the Webhook tab's Recent payloads list. |
Examples
curl
curl -X POST '<YOUR_API_URL>/leads/webhook/<your-secret>' \
-H 'Content-Type: application/json' \
-d '{
"customerName": "Ada Lovelace",
"customerEmail": "[email protected]",
"customerPhone": "+1-202-555-0100",
"message": "Need a quote for a kitchen remodel"
}'Node.js (fetch)
await fetch(
'<YOUR_API_URL>/leads/webhook/<your-secret>',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customerName: 'Ada Lovelace',
customerEmail: '[email protected]',
customerPhone: '+1-202-555-0100',
message: 'Need a quote for a kitchen remodel',
}),
},
);Zapier / Make
1. Add a "Webhooks by Zapier" action and choose "POST". 2. URL: paste the webhook URL from the channel's Webhook tab. 3. Payload Type: JSON. 4. Data: map your trigger's fields to customerName, customerEmail, customerPhone, and message. Extra fields are preserved on the lead. 5. Wrap Request In Array: No. Unflatten: Yes.
Troubleshooting
MISSING_NAME, DUPLICATE, LIMIT_REACHED, etc. The full payload is there for copy.DUPLICATE. Earlier leads do not block new submissions.