Destination

Attribution webhook integration for anything we do not build

Attribi connects to a fixed list of CRMs and ad platforms, and your stack is not on it. The outbound webhook is the answer: give Attribi an HTTPS endpoint and it POSTs a signed JSON event when a lead is captured, when that lead is marked qualified, and when it closes won with revenue attached. It runs one way, from Attribi to you, which is worth knowing before you plan around it.

No credit card required · 5-minute setup

What Attribi does with Webhook

Three events across the funnel

Sends

A lead.created event fires on conversion carrying the person’s full touchpoint history, then lead.qualified and lead.closed_won fire as the deal progresses, the second with the value and currency on it. Each is JSON over HTTPS to one endpoint you nominate.

Signed so you can prove it came from us

Sends

Every request carries an HMAC-SHA256 signature computed over the timestamp and the body together, alongside the timestamp and the event name in their own headers. Signing them together is what stops a captured body being replayed at you later.

A secret you cannot get wrong

Sends

Attribi generates the signing secret rather than letting you type a passphrase, so it is never a reused password. Changing the destination URL keeps the same secret, so you do not have to redeploy your receiver, and rotating it is a separate deliberate click.

Test it before you trust it

Sends

A button on the Connections page sends a real test event to your endpoint and shows you the status code it actually returned. You find out you responded 403 while you are still setting up, rather than from a gap in your data three weeks later.

On the roadmap

Planned, not live today. Listed so you can see what this integration does not do yet, before you sign up.

Read your data back out

Planned Imports

A public API for querying your own attribution data, with keys you manage. Nothing like it exists today: data leaves Attribi only through the destinations we build, which are this webhook, the CRM connections, the Google Sheets export and CSV download.

Not yet available.

Setting up Webhook

  1. 1

    Stand up an HTTPS endpoint

    Attribi refuses anything that is not HTTPS, so no plain HTTP and no local address. Your endpoint should answer quickly and return a 2xx, because Attribi waits five seconds and treats anything slower as a failure. Acknowledge first and do your own work afterwards.

  2. 2

    Paste the URL and copy the secret

    Save the URL on the Connections page and Attribi shows you a signing secret it generated. Copy it into your receiver and verify every request by recomputing the HMAC over the timestamp and the raw body, then comparing in constant time. Reject anything that does not match.

  3. 3

    Send a test event

    Use the test button and read the status code your endpoint returned. Once you see a 2xx, real events start arriving as leads convert and deals progress. Watch for the event name header to tell the three kinds apart, since they carry different fields.

Step-by-step walkthrough: Attribution Webhooks: Prove Receipt, Processing and Reconciliation

What the webhook sends, field by field, and what it does not do

Your stack has a CRM we do not connect to. You want the attribution data anyway. The webhook is how it leaves.

The Attribi attribution webhook integration is outbound only. Attribi POSTs a signed JSON body to one HTTPS URL you choose, on three events: lead.created, lead.qualified and lead.closed_won. Nothing travels the other way. There is no inbound route for stage changes, no retry on failure, and the payload carries raw email and phone today.

What follows is the detail a receiver needs. Every product claim was checked against our code on 12 October 2026.

What is the Attribi attribution webhook integration?

The Attribi attribution webhook integration is a one-way push from Attribi to an HTTPS endpoint you run. It sends one signed JSON request per event, for three events across the funnel, so that a lead's journey and its outcomes can be written into any system Attribi has no native connector for.

The sender is lib/webhook-push.ts. The only two routes under /api/webhook/ store your URL and fire a test event; neither accepts an event from you.

Attribi does have inbound webhook routes, under /api/webhooks/ (plural), but they serve Shopify and our blog publishing tool; posting a stage change to them does nothing. The way to tell Attribi a lead qualified or closed is still the Mark buttons, a connected CRM, Shopify, or the Google Sheets stage tracker. There is no public REST API for reading data back either.

The Attribi webhook data path: Attribi sends lead.created, lead.qualified and lead.closed_won as signed POSTs to one HTTPS endpoint, and nothing comes back

What does each request look like on the wire?

Every delivery is an HTTP POST with a JSON body and five headers that matter.

HeaderValue
Content-Typeapplication/json
x-attribi-eventlead.created, lead.qualified, lead.closed_won or webhook.test
x-attribi-timestampUnix time in whole seconds, as a decimal string
x-attribi-signatureLowercase hex HMAC-SHA256, 64 characters
User-AgentAttribi-Webhook/1.0

The body is always the same envelope: { "event": ..., "sent_at": ..., "data": ... }. sent_at repeats the timestamp header as a string, because it is the value that went into the signature. There is no delivery id or sequence number, so an idempotency key has to be derived from the event name plus the identifying fields under data.

What is in the lead.created payload?

It fires when a visitor converts on a form the Attribi tracker sees, and it carries the person's full journey. An illustrative delivery with one touchpoint (every value here is invented):

{
  "event": "lead.created",
  "sent_at": "1760313600",
  "data": {
    "site_id": "3f1c2b6e-8a0d-4f1e-9c7b-2d5e6f7a8b9c",
    "visitor_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "email": "priya@example.com",
    "phone": "+971501234567",
    "name": "Priya Nair",
    "first_touch": {
      "source": "google",
      "medium": "cpc",
      "campaign": "brand-uae",
      "at": "2026-10-01T09:14:22.108Z"
    },
    "last_touch": {
      "source": "linkedin",
      "medium": "paid",
      "campaign": "q4-retargeting",
      "at": "2026-10-12T18:02:40.551Z"
    },
    "touchpoint_count": 4,
    "session_count": 3,
    "days_to_conversion": 11.37,
    "touchpoints": [
      {
        "id": "7c0e...",
        "session_id": "9d8c...",
        "event_type": "pageview",
        "source": "google",
        "medium": "cpc",
        "campaign": "brand-uae",
        "click_id": "Cj0KCQjw...",
        "click_id_platform": "google_ads",
        "landing_page": "/pricing",
        "referrer": "Direct",
        "timestamp": "2026-10-01T09:14:22.108Z"
      }
    ]
  }
}

There is no lead_id. The identifier is visitor_id, the tracker's first-party cookie value. The two outcome events carry lead_id and no visitor_id, so email is the only key on all three events. Join on email and store both ids as you learn them.

touchpoints is the raw table rows. The conversion route reads them with a select('*'), so every column is in the array (site_id, visitor_id, utm_term, utm_content and two landing_error columns are omitted above), and a new column appears the day a migration adds it. Parse what you need rather than validating a fixed schema.

days_to_conversion is a float. Days from first touchpoint to conversion, unrounded, and null when there was no tracked pageview before the form.

What is in lead.qualified and lead.closed_won?

The two outcome events are small and nearly identical. data holds site_id, lead_id, email, phone, and then closed_won_at, closed_won_value and closed_won_currency (an illustrative value: 18500, "AED"). lead.qualified has the same keys with qualified_ in place of closed_won_. The semantics differ.

Fieldlead.qualifiedlead.closed_won
*_atISO 8601, set by Attribi when it recorded the change, not the CRM's own dateSame
*_valuenull allowed: a lead can be qualified with no estimateRequired, positive number
*_currencynull when value is null; otherwise upper-cased, blank coerced to USDUpper-cased code; blank coerced to USD

The USD coercion is a real trap. Since 5 October 2026 our CRM pulls resolve the organisation's home currency when a deal does not state one, but the manual Mark closed won button and the Shopify ingest can still reach the central function with a blank currency, and it fills in USD. And closed_won_at is when Attribi processed the change: a deal closed in Salesforce on Friday and pulled on Saturday arrives saying Saturday.

When does each event fire, and from where?

TriggerEventFires?
Visitor submits a form the tracker instrumentedlead.createdYes, after the CRM and ad-platform pushes
Meta Instant Form lead ingestedlead.createdYes, with one synthetic touchpoint
Lead created by a CRM pull in pixel-less modelead.createdNo
Lead created by a Shopify order with no prior leadlead.createdNo
Mark qualified button, CRM pull, Shopify checkout, Sheets stage trackerlead.qualifiedYes
Mark closed won button, CRM pull, Shopify paid order, Sheets stage trackerlead.closed_wonYes
Send test event button on the Connections pagewebhook.testYes, owner or admin only

The two "No" rows are the ones to plan around. The lead.created push lives in the conversion fan-out, which only runs for leads the tracker or Meta Instant Forms created. A lead that first appears because your CRM pull found a new contact takes a different path that fires ad-platform lead events but never the webhook, so a pixel-less workspace sees lead.qualified and lead.closed_won for those leads with no lead.created before them. Ordering is fixed: the webhook fires last, after the six ad-platform uploads.

How do I verify the signature without a timing leak?

The signature is an HMAC-SHA256 keyed with your signing secret over the timestamp, a literal full stop, and the raw body, in that order, hex encoded.

import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifyAttribi(rawBody, headers, secret) {
  const ts  = headers['x-attribi-timestamp']
  const sig = headers['x-attribi-signature']
  if (!ts || !sig) return false

  // Reject anything more than five minutes from your clock.
  const skew = Math.abs(Math.floor(Date.now() / 1000) - Number(ts))
  if (!Number.isFinite(skew) || skew > 300) return false

  const expected = createHmac('sha256', secret)
    .update(`${ts}.${rawBody}`)
    .digest('hex')

  const a = Buffer.from(expected, 'utf8')
  const b = Buffer.from(sig, 'utf8')
  if (a.length !== b.length) return false
  return timingSafeEqual(a, b)
}

Here is the checkable thing most verification snippets get wrong, including the sample inside our own Connect Webhook dialog as of 12 October 2026: they compare the two hex strings with a plain inequality operator, which stops at the first differing byte and leaks how much of a forged signature was right. Use timingSafeEqual, and check the lengths first, because Node's crypto.timingSafeEqual throws ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH on unequal lengths instead of returning false (Node.js docs, confirmed by running it on 12 October 2026).

Sign the raw body, not a re-serialised one: Express needs express.raw({ type: 'application/json' }) on that route, and a Next.js route handler needs await request.text() before any .json(). And keep the skew check. The timestamp is inside the signed string, so a captured delivery cannot be replayed past your window; skip the check and the timestamp is decoration.

The secret is 64 hex characters from 32 random bytes, generated by Attribi. Saving a new URL keeps it; only the Rotate secret button changes it, and the old value stops working at once.

What happens when my endpoint fails?

The full failure path is short.

  1. Attribi waits up to 5,000 ms for a response.
  2. Any 2xx is success. Anything else, a timeout included, is a failure.
  3. The failure is reported to Sentry, our error tracker, with the event name and a retryable flag (true for 5xx and network errors, false for 4xx).
  4. Nothing reads that flag. We do not retry, queue, dead-letter or replay. The event is gone.
  5. Nothing is written to the workspace. The health columns behind the red badges on other Connections cards are never touched, so the webhook card shows no health state at all.

So the only delivery feedback is the status line under the Send test event button, and it only covers the test. An endpoint that started returning 403 yesterday looks healthy from inside Attribi. This is an open item on our side. Until it ships, count received events per day on your end and reconcile against the leads list or CSV export.

How do I wire it to a CRM Attribi does not connect to?

The honest use of this webhook is a CRM or tool not on our list. The pattern is the same in Zapier, Make and n8n: receive the raw request, verify the signature, branch on x-attribi-event, and write to the destination keyed on email.

Three Attribi webhook events joined on email: lead.created carries visitor_id, the outcome events carry lead_id, and only email is on all three

A worked example in Zapier, checked against Zapier's help page on 12 October 2026:

  1. Trigger: Webhooks by Zapier, event Catch Raw Hook, not Catch Hook. Only the raw variant includes request headers. Zapier lists the app on Professional, Team and Enterprise, not Free, and caps the raw body at 2 MB.
  2. Paste the generated URL into Attribi's Connect Webhook dialog and copy the signing secret into a Code step constant.
  3. Code by Zapier (JavaScript) with the function above, reading inputData.headers and inputData.raw; throw on a bad signature to stop the Zap.
  4. Paths, one per event. On lead.created, Find or Create a contact by email and write visitor_id and the touch fields into custom fields. On the outcome events, find by email, set the stage, write the value and currency, and store lead_id.
  5. Send a test event from Attribi and read the status code Zapier returned.

Two vendor behaviours to know, both verified on 12 October 2026. Make's custom webhook answers 200 Accepted on receipt, and a webhook attached to no scenario for five days is deactivated and answers 410 Gone, which Attribi treats as a failure and moves on from silently. n8n's Webhook node needs Raw Body on to give you bytes you can sign and Respond: Immediately to stay inside the 5 second budget; its Header auth cannot evaluate an HMAC, so verify in a Code node.

How does the webhook relate to pixel-less attribution?

Pixel-less attribution gets leads and outcomes into Attribi without the tracker, by reading a CRM. The webhook gets events out, and cannot replace either half: it does not read your CRM, and it does not accept a stage change from you. For a pixel-less workspace it carries the outcomes onward as lead.qualified and lead.closed_won, with the CRM's email and no lead.created before them. If what you want is Attribi reading a CRM we do not support, the Google Sheets stage tracker is the right tool.

Does the payload contain raw email and phone?

Yes. All three events send email and phone as stored, and lead.created sends name too. The hashing Attribi does for ad-platform uploads happens on our server and is not applied to the webhook; the privacy page says the same.

That makes your receiver a processor of personal data: TLS only (Attribi refuses a non-HTTPS URL with a 400 at save time), no request bodies in a shared log, a retention policy. A hashed variant (payload_version: 2, with email_hash_standard and phone_hash_digits replacing raw values) is specified in our PII rollout plan and was deliberately not built when the first two stages shipped on 20 August 2026. As of 12 October 2026 it is still unbuilt, so tolerate new keys under data.

What the Attribi webhook does not do

Current as of 12 October 2026:

  • One endpoint per workspace, receiving every event. No per-event routing, no filtering, no staging URL.
  • One delivery attempt, 5 second timeout, no retry, no replay, no health badge.
  • No delivery id or idempotency key, and no common identifier across the three events.
  • No lead.created for leads that originated from a CRM pull or a Shopify order.
  • Raw email, phone and name in the payload; the hashed variant is specified, not shipped.
  • Outbound only. No inbound stage changes, no public API to read data back.

Common questions about this connection

Does the Attribi webhook fire for leads imported from my CRM? Only the outcome events. A lead Attribi first learned about from a CRM pull gets lead.qualified and lead.closed_won when its stage changes, but never lead.created, which is sent from the tracker conversion path and the Meta Instant Forms path only.

Is x-attribi-timestamp in seconds or milliseconds? Whole seconds since the Unix epoch, as a decimal string in both the header and the body's sent_at. A skew check written for milliseconds rejects every delivery as decades old. Compare it to Math.floor(Date.now() / 1000).

How large can a lead.created delivery get? It scales with the touchpoint count, since the full rows are included. An illustrative row like the sample above serialises to about 600 bytes, so 200 touchpoints make a body around 120 KB: under Zapier's 2 MB cap for Catch Raw Hook and n8n's 16 MB default, but not a payload for a single text column.

Why does closed_won_currency say USD when the deal was in another currency? Because the central closed-won function coerces a blank currency to USD. The CRM pulls now resolve the organisation's home currency, but the manual button and the Shopify ingest can still reach the fallback. Confirm against the source system when you see USD from a non-dollar workspace.

The takeaway

Build the receiver for what the webhook is: three signed events, one attempt each, joined on email, with raw contact details inside. Verify on the raw body, acknowledge fast, and count arrivals on your side. Every new workspace starts with 30 days on the Pro plan, no card required, which is enough time to connect the endpoint, fire the test event, and watch a real conversion arrive with its journey attached.

Funnel stages

Stages Attribi supports for Webhook.

  • Lead
  • Qualified
  • Closed Won

Works with

Frequently asked questions

What happens if my endpoint is down when an event fires?

That event is lost. Attribi makes one delivery attempt per event with a five second timeout, records the failure on our side, and does not queue it for a retry or replay it later. There is no dead-letter queue for this path today. Build your receiver to accept quickly and do the real work asynchronously, and treat the webhook as a stream you may miss items from rather than a guaranteed ledger. If a deal must never be missed, the CRM connections are the more reliable route, because those read from your CRM on a schedule and re-read what they could not finish.

How do I match the three events to the same person?

On the email address, which is the only field all three share. The lead.created event identifies the person by visitor id and does not carry a lead id, while lead.qualified and lead.closed_won carry the lead id and do not carry the visitor id. So a receiver that keys on lead id will never see the creation event for that lead, and one that keys on visitor id will never see the outcome. Key on email, and store both ids as you learn them.

How do I verify the signature?

Read the x-attribi-timestamp and x-attribi-signature headers, take the raw request body exactly as received before any JSON parsing, and compute an HMAC-SHA256 of the timestamp, a full stop, and the body, keyed with your signing secret. Hex encode it and compare against the header using a constant time comparison. Reject the request if it does not match, and reject it if the timestamp is far from your own clock, which is what makes replaying an old capture useless.

Can I send different events to different places?

Not today. One workspace has one webhook endpoint and it receives all three event types, distinguished by the x-attribi-event header. If you need to fan out, route on that header inside your own receiver, which is also where any filtering belongs, since Attribi sends every event and offers no subscription options.

Can I pull data out of Attribi over an API instead?

Not yet. There is no public REST API and no customer key management beyond the tracking ingest key, so the webhook is the push half of that need without the pull half. It is on the roadmap and it is not in the product. If what you want is a periodic snapshot rather than a live feed, the Google Sheets export and the CSV download from the reports are the two things that exist today.

Updated

Connect Webhook to Attribi.

Sign up and connect it from the Connections page.

Start Free

No credit card · Free up to 500 visitors a month