Skip to content

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.created event 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.

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)

This is the endpoint a lead vendor uses to push leads into the LMS.

Setup

  1. Go to Integrations > Add Integration and choose the Webhook type.

  2. Saving the integration generates a unique, unguessable token. The integration page shows the full URL:

    https://leads.celebrationmarketing.dev/api/webhooks/leadpost/{token}
  3. 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) or application/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": []
}
  • merged counts leads that matched an existing record (duplicates).
  • errors lists { index, error } for any lead in the batch that failed.
  • 400 means the token is missing or the body couldn’t be parsed. 401 means the token is invalid. 500 means 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.

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.

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.

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.

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 push each new campaign lead to an external system in near real time, as an alternative or supplement to the daily data-feed file.

  1. Go to Settings > Webhooks > New Webhook.
  2. 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 override Content-Type, Content-Length, Host, X-Delivery-Id or X-Webhook-Event.
    • Campaigns. At least one. The webhook fires for leads entering any selected campaign’s segment.
    • Active. Inactive webhooks don’t fire.
  3. 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.

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.

POST {your target URL}
Content-Type: application/json
X-Webhook-Event: lead.created
X-Delivery-Id: 18342
{your custom headers}

Requests are not signed. To authenticate them, add a secret custom header and check it on your end.

{
"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.

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.

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.