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.

To get your URL and secret, open Settings → Leads and pick a channel's Webhook tab.

Endpoint

POST{API_URL}/leads/webhook/{secret}?channel={slug}
  • Content-Type must be application/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 fieldSource keys checked (in order)
customerNamecustomerName, customer_name, name, full_name, fullName
customerEmailcustomerEmail, customer_email, email, e_mail
customerPhonecustomerPhone, customer_phone, phone, telephone, tel
messagemessage, 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.

customerName is required
If none of the configured keys yields a non-empty string, the request is logged as 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.

Example
You have two channels: 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 in general.
  • POST to …/leads/webhook/<general-secret>?channel=vip → lead lands in vip.
  • 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.

We return 200 even on failures so upstream systems don't retry into a duplicate storm. Check the Webhook tab to see what happened.

Event statuses

Every request produces exactly one WebhookEvent row with one of these statuses:

StatusMeaningHow to fix
ACCEPTEDLead was created from the payload.No action needed.
DUPLICATEA 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_REACHEDYour tenant has hit the monthly lead limit for its plan.Upgrade your plan or wait until the next billing cycle.
MISSING_NAMEThe 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_INVALIDThe URL secret didn't match any channel.Copy the current secret from the channel's Webhook tab; it may have been regenerated.
CHANNEL_DISABLEDThe channel that owns the secret is disabled.Re-enable the channel from Settings → Leads.
WEBHOOK_DISABLEDThe 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.
ERRORUnexpected error while processing the payload.Retry. If it persists, contact support with the payload from the Webhook tab's Recent payloads list.

Examples

curl

bash
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)

ts
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

text
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

Lead never appears
Open the channel's Webhook tab and check Recent payloads. The status tells you why — MISSING_NAME, DUPLICATE, LIMIT_REACHED, etc. The full payload is there for copy.
Building a mapping from a real payload
Send one test request, then click Configure fieldsnext to an accepted payload in the Webhook tab to open the Fields tab and map the payload's keys.
Duplicates are matched on email OR phone within 5 minutes
If either matches a lead created in the last 5 minutes, the new request is dropped with status DUPLICATE. Earlier leads do not block new submissions.
Payloads with no email or phone are never deduplicated
Name-only payloads always pass the duplicate check. Repeated identical submissions all create leads.