Zapier, Monday.com and webhooks
Webhooks work in both directions:
- Into Sourcream (webhook intake): your form tool or backend posts each submission to Sourcream, server to server. Capture does not depend on the visitor's browser.
- Out of Sourcream (webhook integration): Sourcream posts every new lead, with its attribution, to a URL of yours, and sends it as an update when a later report of the same submission adds contact details or form fields. Zapier, Make, n8n or your own code take it from there.
Monday.com has no native Sourcream integration yet; it works through Zapier with either direction.
Into Sourcream: webhook intake
Each site has a private intake URL, shown once when the site is created and again whenever you rotate it under Sites, your site, Webhook intake:
https://cdn.sourcream.ai/hooks/YOUR_SECRET_TOKEN
Treat it like a password: anyone with it can create leads for the site. Rotate it if it leaks; the old URL stops working at once.
Send a POST with a JSON or application/x-www-form-urlencoded body, up to 64 KB. Sourcream looks for these keys at any depth, ignoring case, and also accepts the site's renamed field names:
| Key | Use |
|---|---|
sc_visitor_id |
The visitor ID from the hidden field. Links the lead to the visitor's full history on our servers. Send it whenever you can. |
email, name, first_name, last_name, phone, company |
Contact details. |
first_source, first_medium, first_campaign, last_source, last_medium, last_campaign |
Hidden field values, used when the visitor is unknown to our servers. |
event_id |
Your id for the submission. A repeat with the same id is recognised as a duplicate. |
Every other key is stored with the lead as a captured field. Webflow's form webhook (fields under payload.data, id in payload.id) and Typeform's webhook (hidden fields under form_response.hidden, answers under form_response.answers, id in event_id) are understood as they are.
Attribution comes from the visitor's durable record when sc_visitor_id matches one, otherwise from the hidden field values, otherwise the lead is (direct). The response:
{ "ok": true, "id": "lead_k3j9x0a1b2c3d4e5f6g7", "duplicate": false }
The browser script also reports each submission. When both the browser and the webhook report the same submission (same visitor within 10 minutes), Sourcream merges them into one lead and counts one tracked event, so you can use both safely. If the merge adds contact details or form fields, Sourcream sends the lead to your webhook integrations as a lead.updated request with the details added.
Example:
curl -X POST "https://cdn.sourcream.ai/hooks/YOUR_SECRET_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","name":"Jo Citizen","sc_visitor_id":"v_k3j9x0a1b2c3d4e5f6g7","first_source":"google","first_medium":"cpc","first_campaign":"spring","last_source":"google","last_medium":"cpc","last_campaign":"spring","event_id":"form-123"}'
From Zapier
Add an action step Webhooks by Zapier, POST: URL is your intake URL, Payload Type json, and under Data map email, name, sc_visitor_id and the six hidden fields from your form trigger. Map event_id to the form tool's submission id if it has one.
Out of Sourcream: the webhook integration
- In Sourcream, open Integrations and choose Connect on Webhook.
- Enter the destination URL. It must be
https, and must not point atlocalhost, a private network address or a.localor.internalhost. - Sourcream shows the signing secret once. Store it where your receiver can read it.
- Choose which sites send to it under the integration's settings.
For every new lead Sourcream sends:
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Sourcream-Webhook/1.0
X-Sourcream-Signature: t=1760054400,v1=5f2b...e9
X-Sourcream-Event: lead.created
X-Sourcream-Lead-Id: lead_k3j9x0a1b2c3d4e5f6g7
{
"event": "lead.created",
"id": "lead_k3j9x0a1b2c3d4e5f6g7",
"createdAt": 1760054400000,
"test": false,
"site": { "id": "site_a1b2c3d4e5f6g7h8i9j0", "name": "Acme", "domain": "acme.com.au" },
"values": {
"first_source": "google", "first_medium": "cpc", "first_campaign": "spring_sale",
"last_source": "linkedin", "last_medium": "social", "last_campaign": "(none)",
"sc_visitor_id": "v_k3j9x0a1b2c3d4e5f6g7"
},
"attribution": {
"first": { "source": "google", "medium": "cpc", "campaign": "spring_sale", "clickIds": { "gclid": "Cj0KCQ..." }, "landingPage": "/pricing?gclid=Cj0KCQ...", "ts": 1757376000000, "enrichment": { "channel": "Paid Search", "method": "rules" } },
"last": { "source": "linkedin", "medium": "social", "campaign": "(none)", "referrer": "https://www.linkedin.com/", "ts": 1760050000000, "enrichment": { "channel": "Organic Social", "method": "rules" } },
"touches": 4,
"visitorId": "v_k3j9x0a1b2c3d4e5f6g7"
},
"contact": { "email": "[email protected]", "name": "Jo Citizen", "firstName": null, "lastName": null, "phone": null, "company": "Example Pty Ltd" },
"fields": { "message": "Can we talk about pricing?" }
}
values holds the integration's field set (core, or extended on Business plans and above) as plain strings, ready to map. attribution has the full detail. Contact details are only present when the site captures them. Times are epoch milliseconds.
test is true only for requests sent with Send test lead, and false for every real lead. Filter on it (for example with a Zapier Filter step) if test leads should not reach your board. X-Sourcream-Event repeats event, and X-Sourcream-Lead-Id is the lead's id. One lead can reach you more than once: as a lead.updated with more details (below), or as a repeat when your endpoint saved a request but answered after the 10 second timeout and the delivery was retried. So key everything on id (or X-Sourcream-Lead-Id): create a record the first time you see an id, and update that record for every later request with the same id. The body always carries the whole lead as it is now, so a repeat does no harm. Ignore keys you do not recognise: new ones may be added.
Updates: lead.updated
The browser script and your form tool's webhook intake can both report one submission. When the browser's report arrives first, Sourcream may already have sent the lead to you without contact details (with Capture mode None, or a form whose email field it could not see). When the later report, usually the intake call, adds contact details (email, name, phone or company) or form fields to that lead, Sourcream sends it to each of your webhook integrations as an update:
eventislead.updated, and so is theX-Sourcream-Eventheader;idandX-Sourcream-Lead-Idare the same lead id as in any earlier request for the lead;- the body has the same shape, with the lead's current
contactandfields.
When that request goes out depends on where the lead's delivery to that integration stands when the details arrive:
| Delivery to your webhook | What you get |
|---|---|
| Already delivered | The lead again, as lead.updated, queued at once. |
| Being sent at that moment | That request finishes as it is, then the lead goes again as lead.updated about 5 seconds later, since the first request may have left before the details arrived. |
| Not sent yet: waiting in the queue, or waiting to be retried after an error | The next attempt goes out as lead.updated. With no earlier attempt, or none that reached you, it is the first request you get for that lead. |
| Skipped, for example because the integration did not cover the lead's site | Queued again only when the merge adds an email. If the reason it was skipped has gone, it goes out as lead.updated, the first request you get for that lead. |
| Failed, or the integration is disconnected | Nothing. Retrying a failed delivery from the Deliveries log sends the lead with its current details. |
So the rule is: event is lead.updated when Sourcream has delivered the lead to this integration before, or when a merge asked for it to be sent with the new details; otherwise it is lead.created. Send test lead is always lead.created.
Update the record you created for that lead id rather than creating a new one. A lead.updated usually follows the lead's lead.created, but it can also be the first request you get for a lead, when the details arrived before the lead was delivered to you. So when there is no record with that id yet, create one. Never drop a lead.updated because you have not seen its lead.created. Recipe 2 shows this in Zapier.
How Sourcream treats your response:
| Your response | Result |
|---|---|
| Any 2xx within 10 seconds | Delivered. If the body is JSON with an id, it is stored as the external id. |
| 410 Gone | The integration is disconnected. |
| 429 Too Many Requests | Retried after the number of seconds in your Retry-After header (60 seconds when it is missing or not a number of seconds), up to 10 attempts, then failed. |
| 408, 5xx, timeout or network error | Retried with backoff (1 minute, doubling, at most 6 hours apart) up to 10 attempts, then failed. |
| 3xx redirect | Failed, not retried. Redirects are not followed, so save the final URL in the integration's settings. |
| Other 4xx | Failed, not retried (fix the receiver, then retry from the Deliveries log). |
Account admins get one email a day per integration while deliveries fail.
The Send test lead button sends a synthetic lead for [email protected], which is the easiest way to give Zapier a sample.
Verifying the signature
The signature is an HMAC SHA-256, in hex, of the timestamp, a full stop and the raw request body, keyed with your signing secret. Check it against the raw body exactly as received (before any JSON parsing), and reject old timestamps to stop replays.
Node.js:
import crypto from 'node:crypto';
export function verifySourcreamSignature(rawBody, header, secret, toleranceSeconds = 300) {
if (!header) return false;
const parts = {};
for (const item of header.split(',')) {
const i = item.indexOf('=');
if (i > 0) parts[item.slice(0, i).trim()] = item.slice(i + 1).trim();
}
const t = Number(parts.t);
if (!Number.isInteger(t) || !parts.v1) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;
const expected = Buffer.from(crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'));
const given = Buffer.from(parts.v1);
return expected.length === given.length && crypto.timingSafeEqual(expected, given);
}
Python:
import hashlib
import hmac
import time
def verify_sourcream_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = {}
for item in (header or "").split(","):
key, sep, value = item.partition("=")
if sep:
parts[key.strip()] = value.strip()
try:
t = int(parts.get("t", ""))
except ValueError:
return False
if abs(time.time() - t) > tolerance or not parts.get("v1"):
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
A Zapier Catch Hook cannot check signatures without a code step. The hook URL itself is secret, which is enough for most setups.
Monday.com through Zapier
Either recipe works. Both need Text columns on the board: First source, First medium, First campaign, Last source, Last medium, Last campaign (and Sourcream visitor ID if you want it). Status and Dropdown columns reject values that are not predefined, so they fail on new sources.
Recipe 1: the form's hidden fields (no Sourcream integration needed)
This is the setup proven on a Webflow site: the hidden fields travel with the form submission.
- Install the script and check your forms carry the hidden fields (platform guides).
- Zap: trigger your form tool, new submission; action Monday.com, Create Item.
- Zapier only discovers new form fields from a real submission. Publish, submit a test from the live site with UTMs on the URL, then click Test trigger and pick that submission.
- In the Monday.com step, refresh the fields (Re-test forces a fresh board schema) and map each hidden field to its Text column.
Recipe 2: the Sourcream webhook
Use this when the form tool cannot carry hidden fields, or to get Sourcream's channel and enrichment too.
The Zap creates one item per lead and updates it when the lead comes again (lead.updated, or a retried request), so add one more Text column to the board: Sourcream lead ID.
- Zap trigger: Webhooks by Zapier, Catch Hook. Copy its URL.
- In Sourcream, connect a Webhook integration with that URL and press Send test lead.
- In Zapier, Test trigger and pick the test request. Nested keys appear as, for example, Values First Source.
- Search step: Monday.com, Find Items, on your board, for items whose Sourcream lead ID column equals the request's Id. Set Successful if no search results are found? to True, so the Zap carries on for a lead the board does not have yet.
- Add Paths by Zapier with two paths:
- New lead: continues when step 4's Zap Search Was Found Status is false. Action Monday.com, Create Item: map the item name (for example the contact's name or email), the
valuesfields to their Text columns, and the request's Id to Sourcream lead ID. - Known lead: continues when Zap Search Was Found Status is true. Action Monday.com, Update Item on the item step 4 found, with the same columns.
- New lead: continues when step 4's Zap Search Was Found Status is false. Action Monday.com, Create Item: map the item name (for example the contact's name or email), the
- Refresh fields and test the new lead path: the test request creates an item. Then re-test step 4, which now finds that item, and test the known lead path: it updates the same item. (Each Send test lead is a new lead with its own id.)
Do not filter on Event here: a lead.updated can be the first request for a lead, and this Zap handles either event the same way. To keep test leads off the board, add a Filter step after the trigger that continues only when Test is false (and run the tests above before adding it).
Requests for one lead usually arrive at least seconds apart. Zapier runs each one separately, so on rare occasions two runs for one lead both find nothing and create two items with the same Sourcream lead ID. Keep the newer one, which has the later details, and delete the other.
Questions: [email protected]