CRM Outcome Webhook
Integration guide for developers
Send a lead's real disposition back to ClickLens after your sales team works it. A
form-fill that fires a “Lead” conversion looks identical whether it becomes a
customer or gets thrown out as spam, and Smart Bidding and Advantage+ keep buying more of
whatever you feed them. When your CRM marks a lead closed_won
or disqualified, post
that outcome here. ClickLens reconciles it against the conversion it already scored.
This is the unbiased ground truth the submit-time score cannot have: whether a human filled the form is not the same question as whether the lead was worth money.
Get your URL and secret
- Open Settings → CRM Outcome Webhook.
- Click Generate secret. The signing secret is shown once. Copy it before you leave the page — it is never displayed again.
-
Copy the Webhook URL. It contains your
account's webhook id and looks like
https://app.clicklens.io/api/v1/webhooks/crm/clw_<id>.
Regenerating the secret rotates it. Any recipe still signing with the old secret stops working until you update it.
Send an outcome
Make a POST to your
webhook URL with a JSON body and an HMAC-SHA256 signature.
Headers
| Header | Value |
|---|---|
content-type | application/json |
x-clicklens-signature | Hex HMAC-SHA256 of the exact request body, keyed by your secret. |
Body
| Field | Type | Required | Notes |
|---|---|---|---|
outcome | string | Yes | closed_won or disqualified. Case-insensitive. |
clickId | string | One of these three | The gclid, fbclid, msclkid, or ttclid your CRM captured. |
sessionKey | string | One of these three | The ClickLens session key, if you stored it on the lead. |
conversionEventId | string | One of these three | The ClickLens conversion event id, the most precise key. |
value | number | No | The lead's reconciled revenue. Used to restate value on a down-weight. |
Supply at least one of clickId,
sessionKey, or
conversionEventId.
ClickLens matches in that order of precision and reconciles the most recent matching
conversion. For lead-gen, clickId
is the practical key: most CRMs already capture the gclid
in a hidden field.
Sign the body
Compute the signature over the raw bytes you send, not a re-serialised copy. In Node:
import { createHmac } from 'node:crypto';
const body = JSON.stringify({ outcome: 'disqualified', clickId: 'EAIaIQ...' });
const signature = createHmac('sha256', SECRET).update(body, 'utf8').digest('hex');
await fetch(WEBHOOK_URL, {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-clicklens-signature': signature },
body,
}); What each outcome does
| Outcome | Effect |
|---|---|
disqualified | The conversion's verdict is set to retract and re-queued for write-back. On Google and Microsoft Ads it becomes a retraction keyed by click-id; Meta and TikTok have no per-click adjustment API, so the retraction there is report-only. |
closed_won | The conversion is confirmed genuine. A pending suppression is cancelled, so a real lead is never retracted. The recorded outcome also measures how often a flagged lead was in fact real. |
Write-back to your ad platform is in measured rollout.
Today, posting an outcome records the disposition and shows the adjustment ClickLens
would make on your Conversion Protection dashboard; it does not yet change data on the
ad platform. Each gate runs in shadow mode until its false-positive rate is measured on
real traffic, after which a disqualified
outcome on Google or Microsoft Ads is retracted on the next sync. See
Conversion Protection
for the full rollout discipline.
Responses
| Status | Body | Meaning |
|---|---|---|
200 | { "received": true, "reconciled": true, "outcome": "..." } | The matching conversion was reconciled. |
200 | { "received": true, "reconciled": false, "reason": "no_matching_conversion" } | Authenticated, but no conversion matched the key. Not an error — do not retry. |
400 | { "error": "..." } | Missing or invalid outcome, or no key supplied. |
401 | { "error": "..." } | Missing or invalid signature, or unknown webhook id. |
A 200 with
reconciled: false is
deliberate: a click that never converted through ClickLens has nothing to reconcile, and
retrying will not change that.
Recipes
Zapier
- Trigger: your CRM's “Deal stage changed” (or equivalent) event.
- Filter to the stages you treat as
closed_won and disqualified. - Action: Code by Zapier (JavaScript). Build the JSON body, compute the
x-clicklens-signature with crypto.createHmac, and return both. - Action: Webhooks by Zapier → POST to your webhook URL, passing the body and the signature header from the previous step.
Store the secret in a Zapier “Storage” or environment value, not inline in the step.
HubSpot
- Create a Workflow triggered on deal stage entering “Closed won” or “Closed lost”.
- Add a Custom code action (Node 18). Read the deal's stored
gclid and amount, build the signed request as in the Node example above, and POST it. - Put the secret in the workflow's Secrets store and read it with
process.env.
Salesforce
- Add an Apex trigger or Flow on
Opportunity stage change. - In an Apex callout, build the JSON body and compute the signature with
Crypto.generateMac('hmacSHA256', body, secret), then EncodingUtil.convertToHex. - Send the POST with the signature in the
x-clicklens-signature header. Store the secret in a protected Custom Metadata or Named Credential.
Security
The signature is the only thing standing between your conversion data and a forged
outcome. An attacker who could post an unsigned closed_won
would launder a fraud conversion into your ground truth; one who could post a forged
disqualified would
retract real conversions. ClickLens verifies the HMAC in constant time and rejects
anything that does not match before reading the body. Keep the secret server-side, rotate
it if it leaks, and never expose it in client-side code.