Lead webhooks
Every campaign in CloseUp has its own private webhook URL. Any system that can send an HTTP POST can create leads in that campaign: your backend, a landing page builder, or an automation tool such as Zapier, Make or n8n. The lead lands in the campaign, is routed to a rep by the campaign's rules, and is deduplicated against existing leads.
Get your campaign webhook URL
- Open Lead SourcesIn CloseUp, go to Lead Sources and open Zapier, Make & webhooks (under Developers & automation).
- Find the campaignUnder Campaign addresses, every active and paused campaign is listed with its own URL. If the list is empty, create a campaign first in Campaigns.
- Copy the URLUse the copy button next to the campaign. The full URL is your credential. You do not need any other key or header.
The campaign's page in Campaigns also shows its webhook URL, the full field specification, and ready-made code samples in cURL, HTTP, JavaScript, Node.js, Python, PHP and a Postman collection. The samples include any custom fields the campaign declares.
In the examples below, YOUR_CAMPAIGN_WEBHOOK_URL stands for the URL you copied. Use it exactly as copied.
Send a lead
POST YOUR_CAMPAIGN_WEBHOOK_URL
Content-Type: application/json
{
"name": "Dana Levi",
"phone": "0501234567",
"email": "dana@example.com",
"utm_source": "facebook",
"utm_campaign": "summer-sale",
"utm_content": "video-ad-1",
"message": "I'd like a quote for 3 rooms"
}The body can be JSON (application/json), a URL-encoded form (application/x-www-form-urlencoded) or multipart/form-data. A request with no content type is parsed as JSON first, then as URL-encoded.
Only one thing is required: a phone or an email. The name is optional. Without one, the lead is named after its phone or email.
curl -X POST "YOUR_CAMPAIGN_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{"name":"Dana Levi","phone":"0501234567","email":"dana@example.com","utm_source":"facebook"}'// Keep the URL in server-side configuration, never in browser code.
const res = await fetch(process.env.CLOSEUP_CAMPAIGN_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Dana Levi",
phone: "0501234567",
email: "dana@example.com",
utm_source: "google",
utm_campaign: "brand-search",
gclid: "EAIaIQobChMI...",
}),
});
const data = await res.json();
console.log(res.status, data); // 201 { success: true, leadId } or 200 { success: true, deduped: true, leadId }import os
import requests
resp = requests.post(
os.environ["CLOSEUP_CAMPAIGN_WEBHOOK_URL"],
json={
"name": "Dana Levi",
"phone": "0501234567",
"email": "dana@example.com",
"utm_source": "newsletter",
},
timeout=15,
)
print(resp.status_code, resp.json())Accepted fields
CloseUp reads each lead field from the first matching key in the payload. Keys are matched exactly as listed. Values are trimmed, and empty values are ignored.
| Lead field | Accepted keys | Notes |
|---|---|---|
| Name | name, NAME, full_name, fullName | Optional |
| Phone | phone, PHONE, phone_number | Phone or email is required. Israeli numbers are normalized for matching |
email, EMAIL | Phone or email is required | |
| Source | utm_source, source, SOURCE | Falls back to the campaign's default source |
| Campaign name | utm_campaign, campaign, CAMPAIGN | Stored as the campaign label. The lead still belongs to the campaign that owns the URL |
| Content | utm_content, content, CONTENT | Ad or creative |
| External lead ID | lead_id, leadId, leaderid, LEAD ID | Your own ID for the lead |
| Sales representative | sales_representative, SalesRepresentative, salesRep | Stored with the lead's intake details |
| Message | message | The customer's free-text message, shown on the lead's timeline |
Attribution fields are stored with the lead and used for ad reporting and offline conversions: utm_medium, utm_term, and click IDs such as gclid, gbraid, wbraid, fbclid, fbc, fbp, ttclid, li_fat_id and msclkid. Send them as top-level keys.
If your tool nests fields inside a customData object (Make and GoHighLevel do this), CloseUp reads them from there too. A top-level key wins over the same key inside customData.
Custom field names and custom fields
- Field mapping. If your tool uses other key names (for example
mobileorfull-name), add them to the campaign's field mapping in Campaigns. Your names are tried first, and the built-in keys above still work. - Custom fields. A campaign can declare extra fields (budget, product, preferred time). Send them as keys in the payload and they are saved on the lead. A missing required custom field does not reject the lead. The lead is created and the gap is logged.
Responses
| HTTP | Body | Meaning |
|---|---|---|
| 201 | { "success": true, "leadId": "..." } | A new lead was created |
| 200 | { "success": true, "deduped": true, "leadId": "..." } | The contact already exists. The submission was added to the existing lead |
| 400 | { "error": "Invalid body" } | The body could not be parsed |
| 400 | { "error": "At least one of phone or email is required" } | No phone and no email in the payload |
| 401 | { "error": "Unauthorized" } | The URL is wrong, or it was replaced with New address |
| 409 | { "error": "..." } | The contact details conflict with another lead and need manual review |
| 500 | { "error": "..." } | Server error. Safe to retry |
Treat 200 and 201 as success. Retry 500 responses with a backoff. Do not retry 400 or 401 without fixing the request.
Duplicate handling
Before creating a lead, CloseUp looks for an existing lead in your company with the same phone (normalized, so 050-123-4567 matches 0501234567) or the same email. This check covers all campaigns in the company, not just the one that owns the URL.
- No match: a new lead is created, assigned by the campaign's routing rules, and managers are notified. The response is
201. - Match: no second lead is created. The submission is recorded on the existing lead as a new inbound activity, and the lead is marked as returning. New custom field values are merged into the lead without erasing earlier ones. A name captured earlier is never replaced by a blank one. The response is
200withdeduped: trueand the existingleadId.
Because duplicates are merged, it is safe to resend the same lead after a timeout. The second request returns 200 with the same leadId.
Recipe: Zapier
Use any Zapier trigger that produces a new lead (a form tool, a spreadsheet row, an ad platform) and send it to CloseUp with a webhook action.
- Add the triggerPick the app and event that produces the lead, for example a new form entry.
- Add the actionChoose Webhooks by Zapier and the POST event.
- Set the URLPaste your campaign webhook URL into URL. Set Payload Type to
json. - Map the fieldsUnder Data, add rows named
name,phone,emailand, if you have them,utm_source,utm_campaignandmessage. Map each one to the matching field from the trigger. - TestRun the test step. A
201response means the lead is in the campaign. Check it in CloseUp under Lead Sources → Recent deliveries.
Recipe: Make
- Add the trigger moduleStart the scenario with the module that produces the lead.
- Add an HTTP moduleAdd HTTP → Make a request. Set URL to your campaign webhook URL and Method to
POST. - Build the bodySet Body type to Raw and Content type to JSON (application/json). Enter a JSON body with
name,phone,emailand any other fields, mapping values from the trigger module. - Run onceRun the scenario once and check the response status.
201means a new lead,200means it matched an existing lead.
{
"name": "{{1.full_name}}",
"phone": "{{1.phone}}",
"email": "{{1.email}}",
"utm_source": "make",
"message": "{{1.notes}}"
}Recipe: n8n
- Add the trigger nodeStart the workflow with the node that produces the lead.
- Add an HTTP Request nodeSet Method to
POSTand URL to your campaign webhook URL. Store the URL in an n8n credential or variable, not in a shared workflow export. - Send the bodyTurn on Send Body, set Body Content Type to JSON, and add the parameters
name,phone,emailand any others, using expressions from the trigger node. - ExecuteExecute the node and confirm a
201or200response.
{
"name": "={{ $json.name }}",
"phone": "={{ $json.phone }}",
"email": "={{ $json.email }}",
"utm_source": "n8n"
}