Webhooks
The Lead Management System (LMS) has two kinds of webhooks:
- Incoming. Partners and providers push data to us: new leads, CRM appointments and sales, video engagement, and email delivery events.
- Outgoing. We push a
lead.createdevent to a URL you choose (Zapier, n8n, a CRM) whenever a new lead lands in a campaign’s segment.
All URLs below are relative to the production host, https://leads.celebrationmarketing.dev.
Incoming webhooks
Section titled “Incoming webhooks”| Endpoint | Sender | Auth | Purpose |
|---|---|---|---|
POST /api/webhooks/leadpost/{token} |
LeadPost (or any lead vendor) | Token in URL path | Push new leads |
POST /api/matchback/webhook |
CRM via n8n | Authorization: Bearer |
Report an appointment or sale |
POST /api/webhooks/sendspark?token=… |
Sendspark | Token in query string | Video engagement |
POST /api/outbound/webhooks/resend |
Resend | Svix signature | Email delivery events |
POST /api/outbound/webhooks/postal?token=… |
Postal | Token in query string | Email delivery events |
POST /api/outbound/webhooks/listmonk |
ListMonk | HMAC signature header | Email delivery events |
POST /api/webhook/n8n |
Our own n8n workflows | Optional HMAC signature | Workflow results (internal) |
Lead ingestion (LeadPost)
Section titled “Lead ingestion (LeadPost)”This is the endpoint a lead vendor uses to push leads into the LMS.
Setup
-
Go to Integrations > Add Integration and choose the Webhook type.
-
Saving the integration generates a unique, unguessable token. The integration page shows the full URL:
https://leads.celebrationmarketing.dev/api/webhooks/leadpost/{token} -
Give that URL to the vendor. The token is the only credential, so treat the URL as a secret. There is no separate header or signature.
To check the URL is live, send a GET to it. A valid token returns {"status":"ready", ...} with the list of supported field names. An invalid token returns 401.
Request
- Method:
POST - Content type:
application/json(preferred) orapplication/x-www-form-urlencoded - Body: one lead object, or an array of lead objects for a batch
Field names are matched loosely. First Name, First_Name, firstName and first_name all work, and so do singular and plural forms (Email / Emails, Phone / Phones).
Minimum required data. Each lead needs at least one of:
- an email, or
- a phone number, or
- a name (first or last) plus city, state and ZIP
Leads that have none of these are skipped and logged with the field names that were present.
Routing to a client. Send Campaign Id. If it matches an active entry in the client campaign mappings, the lead is assigned to that client. If it doesn’t, the lead is still imported but left unrouted.
Supported fields
| Group | Fields |
|---|---|
| Contact | First Name, Last Name, Email / Emails, Email Hash, Phone Number / Phone / Phones, MobilePhone, WorkEmail, KnownPhoneNumbers, KnownEmails |
| Address | Address, Address 2, City, State, Zip |
| Demographics | Gender, AgeRange, BirthDate, MaritalStatus, PresenceOfChildren, HouseholdIncomeRange, NetWorthRange, HomeownerStatus, EducationLevel, Occupation, PrimaryLanguage, LengthOfResidenceRange |
| Lifestyle | FamilySegment, GenerationGroup, GenerationSegment, HouseholdSegment, Automotives |
| Company (B2B) | CompanyName, CompanyAddress, CompanyCity, CompanyState, CompanyZip, CompanyIndustry, CompanySize, CompanyUrl, CompanyLogoUrl, CompanyLinkedInUrl, CompanyFacebookUrl, CompanyTwitterUrl |
| Professional | JobTitle, JobRole, JobStartDate, Skills, LinkedInUrl, LinkedInId, FaceBookUrl, FaceBookId, TwitterUrl, TwitterUsername, GitHubUrl, GitHubUsername |
| Website visit | VisitDetails (object or JSON string, may contain PageVisits and any company/professional field above) |
| Tracking | Campaign Id, Campaign Name, Audience Id, Audience Name, Send Date, Record Id, Source |
Example
{ "First Name": "Jane", "Last Name": "Smith", "Email": "jane@example.com", "Phone Number": "512-555-0100", "Address": "123 Main St", "City": "Austin", "State": "TX", "Zip": "78701", "HomeownerStatus": "Own", "HouseholdIncomeRange": "$100,000 - $149,999", "Campaign Id": "48213", "Campaign Name": "Austin Roofing", "Record Id": "lp_9f2c1a"}Response (200)
{ "success": true, "received": 1, "imported": 1, "merged": 0, "skipped": 0, "routedToClient": 1, "unrouted": 0, "errors": []}mergedcounts leads that matched an existing record (duplicates).errorslists{ index, error }for any lead in the batch that failed.400means the token is missing or the body couldn’t be parsed.401means the token is invalid.500means a server error, which is safe to retry.
Every request creates an import log (visible under the integration) and starts lead processing (segmentation, assignment, deployment) in the background.
CRM matchback
Section titled “CRM matchback”Reports a single appointment or sale from a client’s CRM so it can be matched against leads we delivered. Usually called from an n8n workflow.
Auth: Authorization: Bearer {N8N_API_KEY}
Request body
| Field | Required | Notes |
|---|---|---|
clientId |
Yes | LMS client ID |
email or phone |
At least one | Used for matching |
firstName, lastName |
No | Improves fuzzy matching |
streetAddress, city, state, zip |
No | Improves fuzzy matching |
eventDate |
No | Appointment or sale date |
outcome |
No | sale records a sale. Anything else records an appointment. |
saleAmount |
No | Number or string |
campaignId |
No | Narrows matching to one campaign |
{ "clientId": 12, "firstName": "Jane", "lastName": "Smith", "email": "jane@example.com", "phone": "512-555-0100", "streetAddress": "123 Main St", "city": "Austin", "state": "TX", "zip": "78701", "eventDate": "2026-10-20", "outcome": "sale", "saleAmount": 14500}The record is matched against the client’s leads from the last 90 days. A match at 90% confidence or higher automatically suppresses the lead so it isn’t marketed to again. Results are grouped into a monthly “Webhook — Mon YYYY” matchback upload, so they show up in normal reporting.
Response (200)
{ "matched": true, "confidence": 0.95, "method": "email_exact", "leadId": 48211, "status": "auto_approved", "suppressed": true}method is one of email_and_phone, email_exact, phone_exact, name_address_fuzzy or name_zip_fuzzy. When nothing matches, matched is false, method and status are unmatched, and leadId is null.
Sendspark video engagement
Section titled “Sendspark video engagement”Records when a prospect views, plays, finishes, or clicks the CTA on a personalized Sendspark video.
Setup. In the Sendspark dashboard, add this webhook URL (Sendspark doesn’t sign requests, so the secret goes in the query string):
https://leads.celebrationmarketing.dev/api/webhooks/sendspark?token={SENDSPARK_WEBHOOK_SECRET}Tracked events: video_viewed, video_played, video_watched, video_cta_clicked. Other Sendspark events are accepted and ignored.
Fields we read: eventType, campaignId (the per-prospect video ID), dynamicCampaignId, contactInfo.contactEmail, and watchPercentage. An event needs either campaignId, or both contactInfo.contactEmail and dynamicCampaignId, to be matched to a lead.
Only the first occurrence of each event is kept, along with the highest watch percentage seen. The response is { recorded, ignored, unmatched, failed }. It only returns 500 when every event in the request failed on our side, so Sendspark retries.
Email delivery events
Section titled “Email delivery events”These receive delivery outcomes for outbound email sequences. Bounces, complaints and unsubscribes suppress the lead.
| Provider | URL | Auth | Events mapped |
|---|---|---|---|
| Resend | /api/outbound/webhooks/resend |
Svix headers (svix-id, svix-timestamp, svix-signature) signed with RESEND_WEBHOOK_SECRET. Timestamps older than 5 minutes are rejected. |
email.delivered, email.opened, email.clicked, email.bounced, email.complained, email.failed |
| Postal | /api/outbound/webhooks/postal?token={POSTAL_WEBHOOK_SECRET} |
Query-string token (falls back to OUTBOUND_WEBHOOK_SECRET) |
MessageSent (delivered), MessageDeliveryFailed, MessageBounced, MessageHeld |
| ListMonk | /api/outbound/webhooks/listmonk |
X-Listmonk-Signature: sha256=<hex HMAC-SHA256 of raw body> using OUTBOUND_WEBHOOK_SECRET |
delivered, opened, clicked, bounced, complained, unsubscribed |
Each provider is configured with its standard webhook format; nothing custom is needed beyond the URL and secret. For ListMonk, the message custom_id must be lead_{leadId}_{campaignId}_{sequenceStateId}, which the sequence sender sets automatically.
n8n workflow callbacks (internal)
Section titled “n8n workflow callbacks (internal)”POST /api/webhook/n8n is how our own n8n workflows report results back to the LMS. External partners don’t use it.
If N8N_WEBHOOK_SECRET is set, requests must include x-n8n-signature with the hex HMAC-SHA256 of the raw body.
{ "event": "lead.validation.complete", "timestamp": "2026-10-23T14:02:11Z", "data": { "leadId": 48211, "isValid": true, "emailValid": true, "phoneValid": true }, "workflowId": "abc123", "executionId": "9876"}| Event | Key data fields |
|---|---|
offer.extraction.complete |
offerId, title, description, discountType, discountValue, promoCode, termsAndConditions, expiresAt, confidence |
offer.extraction.failed |
offerId, error, reason |
lead.validation.complete |
leadId, isValid, emailValid, phoneValid, addressValid, validationResults |
lead.enrichment.complete |
leadId, enrichmentData, source |
campaign.status.update |
campaignId, status, metrics |
email.sent / delivered / opened / clicked / bounced |
campaignId, leadId, emailId, status |
video.created |
campaignId, leadId, videoUrl, thumbnailUrl |
direct-mail.queued / sent / delivered / returned |
campaignId, leadId, trackingNumber, carrier, status |
qr.scan.processed |
scanId, qrCodeId |
Outgoing webhooks
Section titled “Outgoing webhooks”Outgoing webhooks push each new campaign lead to an external system in near real time, as an alternative or supplement to the daily data-feed file.
- Go to Settings > Webhooks > New Webhook.
- Fill in:
- Name. For your reference, e.g. “Zapier — new roofing leads”.
- Target URL. Must be
https. Localhost, private, link-local and CGNAT addresses are rejected, including hostnames whose DNS resolves to one. - Custom headers (optional). Use these for the receiver’s auth, e.g.
Authorization: Bearer …. Values are masked in the UI after saving. You can’t overrideContent-Type,Content-Length,Host,X-Delivery-IdorX-Webhook-Event. - Campaigns. At least one. The webhook fires for leads entering any selected campaign’s segment.
- Active. Inactive webhooks don’t fire.
- Click Send test on the webhook page to send a sample payload. Every column is filled in, so you can map fields in Zapier or n8n.
When it fires
Section titled “When it fires”There is one event, lead.created. It’s sent once per webhook, campaign and lead when a lead is newly added to the campaign’s segment. That happens on ingestion and during segment recalculation. A sweep every 30 minutes catches anything missed in the last 7 days, without going back further than the webhook’s creation date. The same lead is never sent twice to the same webhook for the same campaign.
The payload is a snapshot taken when the lead joined the segment. Later edits to the lead aren’t re-sent.
Request
Section titled “Request”POST {your target URL}Content-Type: application/jsonX-Webhook-Event: lead.createdX-Delivery-Id: 18342{your custom headers}Requests are not signed. To authenticate them, add a secret custom header and check it on your end.
Payload
Section titled “Payload”{ "event": "lead.created", "deliveryId": 18342, "occurredAt": "2026-10-23T18:14:52.000Z", "campaign": { "id": 57, "name": "Austin Roofing — Oct" }, "segmentId": 112, "lead": { "id": 48211, "email": "jane@example.com", "firstName": "Jane", "lastName": "Smith", "company": "Acme Corp", "phone": "555-0100", "address": "123 Main St", "city": "Austin", "state": "TX", "zip": "78701", "intentTopics": ["Roofing"], "score": 82 }, "fields": { "RecordId": 48211, "ExternalSourceId": "ConvertMaxPro", "LastActivityDate": "2026-10-07T19:14:52-04:00", "FirstName": "Jane", "LastName": "Smith", "Email": "jane@example.com", "Phone": "555-0100", "OtherPhones": "555-0101, 555-0102", "Address": "123 Main St", "Address2": "Apt 4", "City": "Austin", "State": "TX", "Zip": "78701", "IntentTopics": ["Roofing"], "IntentScore": 82, "QR Code": "abc12xyz" }}The fields example above is shortened.
Top-level keys
| Key | Description |
|---|---|
event |
Always lead.created |
deliveryId |
Unique per delivery. Matches the X-Delivery-Id header. Use it to de-duplicate retries. |
occurredAt |
ISO 8601 UTC time the lead joined the segment |
campaign |
{ id, name } |
segmentId |
Segment the lead joined |
lead |
Stable, compact lead summary. Every key is always present, with null if unknown. |
fields |
The same columns as the client’s daily data-feed file, keyed by its CSV header names |
fields columns. Every key for the segment type is present, with null when there’s no value. Dates use Eastern time ISO format.
| Group | Columns |
|---|---|
| Record | RecordId, ExternalSourceId, LastActivityDate |
| Contact | FirstName, LastName, Email, Phone, OtherPhones, Address, Address2, City, State, Zip, County |
| Demographics | Gender, AgeRange, MaritalStatus, PresenceOfChildren, IncomeRange, NetWorth, HomeownerStatus, EducationLevel, Occupation, PrimaryLanguage, LengthOfResidence |
| Lifestyle | FamilySegment, HouseholdSegment, GenerationGroup, GenerationSegment, Automotives* |
| Company | CompanyName, Domain, Industry, CompanySize, RevenueRange, CompanyLinkedInUrl |
| Professional | JobTitle, JobRole, BusinessEmail, LinkedInUrl, Skills* |
| Website visit | InitialPageViewDate, LastPageViewDate, ImpliedSecondsOnSite, TotalSecondsOnSite, TotalPageViews, PageUrlsVisited |
| Intent | IntentTopics†, IntentScore† |
| QR | QR Code |
* Not included for buyer-intent segments (intent_b2b, intent_b2c).
† Not included for website-visitor segments.
Intent topics are filtered to the segment’s topic whitelist, or the client’s if the segment has none. One client never sees another client’s intent topics.
What your endpoint should return
Section titled “What your endpoint should return”Return any 2xx quickly. The request times out after 10 seconds.
| Your response | What we do |
|---|---|
2xx |
Marked delivered |
429 or 5xx, timeout, network error |
Marked failed and retried with backoff, up to 6 attempts total, then dead |
Other 4xx |
Marked dead immediately, no retry |
3xx redirect |
Marked dead. Redirects are never followed. |
Deliveries also go dead if the webhook is deactivated or deleted, or if its URL stops passing the safety checks.
Monitoring and redelivery
Section titled “Monitoring and redelivery”The webhook’s page in Settings > Webhooks shows recent deliveries with status, attempts, last HTTP status, error, and response time. Failed or dead deliveries have a Redeliver button, which resets them and sends the original payload again.