Webhook

Custom Webhook best practices

Custom webhooks let you send any event from any system into FunnelTrack. That covers CRM stage changes, backend cron jobs, checkouts on platforms without a native integration, or anywhere your business data lives. This guide covers how to design a payload that matches to the right contact, avoids duplicates, and reaches every ad platform with strong quality signals.

When to use a custom webhook

Native integrations first when we have them. Custom webhooks fill every other gap.

Check the sources list first. If we already integrate with your source (Jotform, ClickFunnels 2.0, HighLevel, Calendly, CallRail, Elementor, Gravity Forms, Typeform, Unbounce, Shopify, WooCommerce, Stripe), use that path. Native integrations handle payload shape, session stitching, and field mapping for you.

Reach for a custom webhook when:

  • Your source system isn't natively supported (many CRMs, billing systems, custom apps)
  • You need to send CRM events like opportunity stage changes, account created, deal won, or subscription upgraded
  • You're sending backend events like batch imports, offline conversion uploads, or scheduled cron
  • You want granular control over which specific event names fire and when
Sending webhooks through Zapier, Make, or n8n

You don't have to code the webhook yourself. Any automation platform that can POST JSON works. In Zapier, use the Webhooks by Zapier → POST action. In Make (formerly Integromat), use the HTTP module. In n8n, use the HTTP Request node.

Point the action at your FunnelTrack webhook URL, set the payload format to JSON, and map the fields described in this guide from your trigger app (HubSpot deal, Salesforce opportunity, Airtable record, Google Sheet row, whatever). This is the fastest path for non-technical setups because your CRM already has all the identity fields ready to map.

Web vs offline

Which type your event is decides how it gets sent to ad platforms. Getting it wrong lowers match quality.

Every event is either web or offline. The difference isn't where the event was fired from. It's whether the visitor was in a browser session on your site at the moment they converted.

Web events happen in a browser session. The visitor is on your site (or a linked site you control) when the event fires. Examples:

  • On-site account creation (account_created)
  • Checkout completed in the browser
  • Form submission on a page where the pixel is loaded

Offline events happen outside a browser session. Examples:

  • CRM stage changes like deal_won or opportunity moved to Closed Won
  • Phone-based sales, in-person transactions, POS closes
  • Batch imports from a system of record (legacy database, spreadsheet backfill)
  • Subscription rebills, refunds, lifecycle updates
You pick web or offline when you create the webhook

You don't include a web/offline flag in the payload itself. When you create a custom webhook event in FunnelTrack, the Create Webhook dialog has an Action Source toggle where you pick Web or Offline. That choice sets the action_source FunnelTrack forwards to every destination for events sent to this URL.

If a single trigger needs to fire as both (rare), create two separate webhooks — one Web, one Offline — and have your sender POST to the appropriate URL depending on context.

Required fields on every payload

The minimum for a webhook FunnelTrack can process.

FieldRequiredWhat it does
event_nameRequiredA human-readable, past-tense name that describes what happened. This shows up in the Pipeline report, usage log, and Milestone cards. Prefer account_created over signup_v2. Describe the trigger, not the downstream destination.
event_idRequiredA stable unique identifier used for deduplication. Use your source system's natural key: order ID, opportunity ID, subscription ID, invoice ID. If a retry sends the same payload twice with the same event_id, FunnelTrack sees the duplicate and only fires one downstream conversion.
event_timeRequiredWhen the event actually happened, not when the webhook fires. Ad platforms weight recency and enforce time windows based on the click date. Sending a stale timestamp as "now" misattributes to the wrong clicks. See the Timestamp format section below.
user_dataRequiredA nested object with the customer's identity fields. See the User data section below. At minimum, send email or phone. Without one of these, the event has no way to match to a person.
valueIf applicableThe monetary value of the conversion, as a number. No currency symbol, no thousands separators. 4200 or 4200.00, never "$4,200.00". Include for revenue events. Skip for non-monetary events like account_created.
currencyIf value setISO 4217 code such as USD, EUR, GBP, or CAD. Required whenever value is present. Without it, ad platforms reject the value or default it to their account default, which may not match yours.

User data

More identity signals mean better match quality on every ad platform. Send raw values. FunnelTrack hashes them for you.

FieldRequiredWhat it does
emailRecommendedHighest match rate on every platform. Send raw (lowercase and trimmed is fine, unhashed).
phoneRecommendedSecond-highest match rate. E.164 format works best (+14155550198) but any format works. FunnelTrack normalizes before hashing.
first_nameOptionalRaw plaintext. Improves Meta match quality.
last_nameOptionalRaw plaintext. Improves Meta match quality.
cityOptionalRaw plaintext. Feeds Enhanced Conversions.
regionOptionalState or province code (AL, CA, ON) or full name. Either works.
postal_codeOptionalZIP or postal code, raw.
countryOptionalISO 3166-1 alpha-2 code (US, CA, GB) is preferred. Full names are accepted and normalized.
external_idOptionalYour source system's own contact or user ID: CRM contact ID, account ID, Shopify customer ID, subscriber ID. Feeds Meta's external_id field so the ad platform can match the same person across devices even without cookies.
Never hash on your end

Send raw plaintext identity fields over HTTPS to your FunnelTrack webhook URL. FunnelTrack strips them out, hashes with SHA-256 using the exact normalization each destination expects, and forwards only the hash downstream. If you pre-hash, we can't re-normalize, so match quality drops.

FunnelTrack never logs or stores plaintext identity data. The hashing step happens before storage.

Web events: matching to the visitor's session

How to attach the visitor's pre-conversion pageview data (landing page, click IDs, UTMs) to your webhook event.

For a web event, include the visitor's session identifiers so FunnelTrack can match the webhook to their earlier pageviews. The strongest signals are FunnelTrack's own cookies.

FieldRequiredWhat it does
web_data.ft_uidWeb onlyFunnelTrack's 400-day contact identifier (cookie name _ft_uidon the customer's domain). Best match. Resolves directly to the contact.
web_data.ft_sidWeb onlyFunnelTrack's 30-minute session identifier (cookie name _ft_sid). Resolves to the exact pre-conversion session with its landing page, referrer, and captured click IDs.
web_data.ip_addressWeb onlyVisitor IPv4 or IPv6. Used for fingerprint-based fallback matching if cookies didn't come through, and forwarded to Meta in user_data.
web_data.user_agentWeb onlyThe visitor's browser User-Agent string. Paired with ip_address for the fingerprint fallback. Forwarded to Meta.
web_data.page_urlWeb onlyThe URL where the conversion happened (checkout success page, signup completion page). Feeds Meta's event_source_url.
web_data.page_referrerOptionalThe URL that referred the visitor to the conversion page.

How to read _ft_uid and _ft_sid

There are three ways to grab these cookies depending on where your webhook fires from. Pick whichever matches your setup.

A. Client-side JavaScript (form embeds, on-page fetch)

If your webhook fires from browser JavaScript, read the cookies directly:

function getCookie(name) {
  const m = document.cookie.match('(^|;)\\s*' + name + '=([^;]+)');
  return m ? decodeURIComponent(m.pop()) : '';
}

fetch('https://your-webhook-proxy.example.com/convert', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    event_name: 'account_created',
    event_id: 'USR-8472',
    // ... other fields ...
    web_data: {
      ft_uid: getCookie('_ft_uid'),
      ft_sid: getCookie('_ft_sid'),
      page_url: window.location.href
    }
  })
});

B. Server-side on the same domain (WordPress, Rails, Django, Node)

If your webhook fires from your backend on a page load (thank-you page render, post-checkout webhook fired server-side), read the cookies from the inbound request:

// PHP (WordPress, custom PHP)
$ft_uid = $_COOKIE['_ft_uid'] ?? '';
$ft_sid = $_COOKIE['_ft_sid'] ?? '';
# Python (Django, Flask)
ft_uid = request.COOKIES.get('_ft_uid', '')
ft_sid = request.COOKIES.get('_ft_sid', '')
// Node.js (Express, Next.js API route)
const ft_uid = req.cookies['_ft_uid'] || '';
const ft_sid = req.cookies['_ft_sid'] || '';

C. URL params on cross-domain handoffs

When a visitor moves from your main site to a separate domain (for example store.example.com to checkout.example.com), cookies don't travel with them. The FunnelTrack pixel detects cross-domain links to your configured domain list and appends _ft and _ft_uid as URL parameters. On the destination page, parse them:

const url = new URL(window.location.href);

// Prefer URL params (fresh cross-domain handoff), fall back to cookies.
const ft_uid = url.searchParams.get('_ft_uid') || getCookie('_ft_uid');

// _ft param format is: <sid>.<timestamp>
const ft = url.searchParams.get('_ft');
const ft_sid = ft ? ft.split('.')[0] : getCookie('_ft_sid');
Configure your cross-domain list first
Cross-domain URL decoration only fires for domains listed in your FunnelTrack Pixel settings under Additional Domains. Domains not on that list are treated as external and never get URL params appended. Add every domain in your funnel to that list before relying on option C.

Offline events: matching by identity

No browser cookies to lean on. Matching runs entirely on the identity fields you send.

For an offline event, FunnelTrack matches the incoming event to an existing contact via user_data. Email and phone do most of the work. When a match hits, the contact's prior session context (landing page, first-touch source, captured Meta and Google click IDs) gets attached to the outbound dispatch automatically. You send identity. You get attribution enrichment for free.

Every offline event needs email or phone
Without one of these, the event lands anonymous. It cannot join to a contact and cannot benefit from prior-session enrichment. It still fires as a raw conversion to your destinations, but with lower match quality (hash matching alone, no cross-device signals).

Optional but powerful: ft_uid if you stored it

If your source system (CRM, custom app) captured the FunnelTrack visitor's _ft_uid at lead-form time, include it in the payload when you later fire an offline conversion for that same person (deal won, contract signed, subscription upgraded):

{
  "event_name": "deal_won",
  "user_data": {
    "email": "marcus.hayes@example.com",
    "phone": "+14155550198",
    "ft_uid": "b5c6d7e8-1a2b-4c3d-9e5f-6a7b8c9d0e1f.2f3e4d5c6b7a8f9e"
  }
}

This is the strongest offline match. Better than email or phone alone (both can miss on typos, work-vs-personal email switches, or number changes). If you're building an offline pipeline from a CRM you control, plan to capture _ft_uid at lead-form time and store it as a custom field on the contact record. Every offline conversion webhook after that carries a perfect join back to the original web session.

Originating click IDs for Google Ads Enhanced Conversions

If your source system captured click IDs at lead-form time (gclid, fbclid, wbraid, gbraid, msclkid, ttclid), send them with the offline event:

{
  "event_name": "deal_won",
  "user_data": { "email": "marcus.hayes@example.com" },
  "click_ids": {
    "gclid": "Cj0KCQjw...",
    "fbclid": "IwAR3..."
  }
}

Google Ads Enhanced Conversions for Leads matches offline uploads to clicks primarily via gclid. If you don't send the originating click ID, Google falls back to identity-hash matching, which has a lower match rate.

Attribution windows to be aware of
  • Meta CAPI: offline conversion match window is about 62 days from the original click
  • Google Ads: Enhanced Conversions for Leads accepts uploads within about 90 days of the click
  • TikTok Events API: about 28 days

Always send the real event_timefor when the conversion actually happened, not when the webhook fires. Batch-uploading last week's deals timestamped as "now" puts them outside the correct attribution windows.

Timestamp format

Send event_time as a Unix timestamp in seconds (integer, UTC). Every language has a one-line function for this.

{
  "event_time": 1785276396
}

Webhook format

Protocol basics your sender must respect.

  • HTTP method: POST only. GET, PUT, PATCH, DELETE are rejected.
  • Content-Type: application/json is recommended. We also accept application/x-www-form-urlencoded for legacy senders. Bracket-notation nested keys (user_data[email]) get flattened server-side.
  • Encoding: UTF-8. Non-UTF-8 encodings will be rejected.
  • HTTPS only.Every FunnelTrack webhook URL is HTTPS. Don't downgrade to HTTP.
  • Payload size:under 100 KB is ideal. 1 MB is the hard cap. Don't send base64-encoded receipts, images, or full call transcripts.
  • Response: we respond with 200 OK on accepted, 400 on malformed, 401 on bad API key, 413 on oversized payload, and 429 on rate-limited. Retry on 429 and 5xx only.
  • Retry policy: if you retry, use the same event_id. FunnelTrack sees the duplicate and won't create a second conversion.

Custom parameters

Any extra key-value pairs you want forwarded to destination event params.

Anything under a top-level custom_paramsobject becomes available for field mapping to destination event parameters (Meta's custom_data, GA4 event params, TikTok properties). Use it for:

  • Lead scores, deal stages, subscription tiers, or any other attribute the ad platform can bid on
  • Internal categorization your team uses (sales rep, source campaign, funnel step)
  • Product or plan identifiers for retargeting audience membership
{
  "custom_params": {
    "plan_tier": "pro",
    "lead_score": 87,
    "sales_rep": "j_ortega",
    "referral_code": "PARTNER-24"
  }
}

Filter conditions

Fire only when the payload matches criteria you set.

Configure filter conditions on the event card so the webhook only creates events when the payload matches. Common patterns:

  • Stage filter: stage == "Closed Won". Only fire on wins, not every stage change.
  • Value floor: value > 500. Only fire for meaningful conversions.
  • Type filter: product_type != "internal_test". Exclude non-customer transactions.

Filters run before dedup, dispatch, and usage counting. Filtered events show up in the usage log as filtered (not failed or received) so you can see which criteria are firing.

Testing checklist

Before flipping a new webhook live.

  1. Send a test payload. Use curl, Postman, or your source system's test fire. Verify a 200 OK response.
  2. Check the inbound row in Usage Log. Confirm the payload arrived and shows the correct event name.
  3. Verify field mapping. Click into the log row. Confirm identity fields, revenue, and event parameters are populated the way you expected.
  4. Enable destinations one at a time. Flip Meta on first. Send another test. Verify it appears in Events Manager Test Events. Then GA4. Then Google Ads. Never enable all at once for a new event.
  5. Confirm dedup works. Send the exact same payload twice. The second call should show as deduped in the usage log, not counted as a second conversion.
  6. Confirm filters work.Send a payload that shouldn't match your filter conditions. Verify it lands as filteredand doesn't reach any destination.

Security

Your webhook URL is a credential.

  • Treat the webhook URL as a secret.It contains an API key that authenticates every submission. Don't paste it in public docs, screenshots, or shared spreadsheets.
  • HTTPS only.Every FunnelTrack webhook URL is HTTPS. Don't downgrade to HTTP.
  • Rotate if leaked. Regenerate the endpoint from the Webhooks settings page. The old URL immediately stops accepting submissions.
  • Filter as soft auth.If you're worried about spurious submissions, add a filter like api_source == "our_crm" and only accept payloads from your intended sender.

Full example: web event (account_created)

A user creates an account on your site. Fired from the browser after the signup form succeeds.

{
  "event_name": "account_created",
  "event_id": "USR-8472",
  "event_time": 1785276396,
  "user_data": {
    "email": "sarah.chen@example.com",
    "phone": "+12065550142",
    "first_name": "Sarah",
    "last_name": "Chen",
    "city": "Portland",
    "region": "OR",
    "postal_code": "97205",
    "country": "US",
    "external_id": "CRM-77321"
  },
  "web_data": {
    "ft_uid": "a3f2b8c1-9d4e-4f56-8a7b-1c2d3e4f5060.7e8f9a0b1c2d3e4f",
    "ft_sid": "4c8b1a2f-5d6e-4f7a-8b9c-0d1e2f3a4b5c",
    "ip_address": "203.0.113.42",
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...",
    "page_url": "https://example.com/signup/confirm"
  },
  "custom_params": {
    "plan_tier": "pro",
    "signup_source": "google_ads"
  }
}

Full example: offline event (deal_won)

A sales rep marks an opportunity as Closed Won in the CRM. Fired from the CRM's webhook automation or a Zap.

{
  "event_name": "deal_won",
  "event_id": "OPP-38472",
  "event_time": 1785276396,
  "value": 4200.00,
  "currency": "USD",
  "user_data": {
    "email": "marcus.hayes@example.com",
    "phone": "+14155550198",
    "first_name": "Marcus",
    "last_name": "Hayes",
    "city": "Austin",
    "region": "TX",
    "postal_code": "78701",
    "country": "US",
    "external_id": "CRM-42196",
    "ft_uid": "b5c6d7e8-1a2b-4c3d-9e5f-6a7b8c9d0e1f.2f3e4d5c6b7a8f9e"
  },
  "click_ids": {
    "gclid": "Cj0KCQjw...",
    "fbclid": "IwAR3..."
  },
  "custom_params": {
    "opportunity_stage": "Closed Won",
    "sales_rep": "j_ortega",
    "deal_source": "inbound_lead"
  }
}
Notice the ft_uid on the offline event
This CRM captured _ft_uidwhen the prospect originally filled out the lead form (weeks or months ago), stored it as a custom field on the contact, and now sends it back on the offline conversion. That's the strongest offline join possible. The deal_won event connects directly to the original web contact and inherits their full attribution history.

Ready to set up your first custom webhook?

Open the Webhooks section of your FunnelTrack workspace to create an endpoint, then follow this guide to design your payload.

Configure your Custom Webhooks →

Need a hand?

If anything in this guide didn't match what you're seeing in your source system or FunnelTrack, open a support ticket in your dashboard and we'll get you sorted.