SurfacedBySurfacedBy Docs

Conversion Tracking

Credit purchases, renewals, trials, and signups to the AI platform that sent the visitor.

Ask an AI:Open in ChatGPTOpen in Claude

Conversion tracking credits sales and other conversions to the AI platform that brought the visitor to your site. When someone arrives from ChatGPT, Perplexity, Claude, or another AI platform and later buys, renews, starts a trial, or signs up, the conversion is credited to that platform.

Results appear on the Tracking page, which also covers AI visitors and AI crawlers. This page covers installing a tracker, reporting conversions, and how credit is assigned.

Conversion tracking needs a paid plan or an active trial; on other plans the Tracking page shows a locked preview. Setting it up requires the Admin role or higher. Once a collector secret or the conversions webhook secret exists, any member who can open the domain, Viewers included, can reveal it to finish an install; creating the first one and rotating it need an admin. See Tracking.

How attribution works

When a visitor first arrives from an AI platform, the tracker stores a first-party cookie named sb_attr on your site. It holds an anonymous visitor ID, the AI platform the visitor came from, and the time of that first AI visit. The cookie expires 13 months after the first AI visit and is not extended by later visits.

When the visitor later converts, the conversion is matched to that visitor and credited to the AI platform in the cookie:

  • The most recent AI platform wins. If a visitor arrives from ChatGPT, later returns from Perplexity, and then buys, the sale is credited to Perplexity.
  • Direct visits, search, and other non-AI referrers never replace the AI platform in the cookie.
  • Only recognized AI platforms earn credit. Conversions with no AI visit behind them are counted as other channels.

The headline numbers on the Tracking page always use this last-touch rule. The conversion journey view can split credit across several AI visits for analysis (see Attribution models), but it never changes the headline numbers.

If a visitor who arrived from an AI platform, on that visit or an earlier one, lands with a Google Ads gclid, Meta fbclid, Microsoft Ads msclkid, or TikTok ttclid parameter, the tracker stores it in the same cookie. IDs longer than 256 characters are ignored. Click IDs from visitors with no AI visit on record are not stored. SurfacedBy does not send these to ad platforms. When you forward the cookie through the server webhook, the click IDs are saved with the conversion, and your own checkout server can read the cookie and pass them to each ad platform's conversion API.

Install a tracker

Open Tracking for the domain. Before any tracker reports in, the page shows the install options and highlights the one that fits your site when it can detect it. After setup, Manage installation in the page header opens the same options. Every method uses the domain's Site ID, shown on the page.

The browser snippet, WordPress plugin, Cloudflare tracker and NGINX tracker use the same sb_attr cookie, so running more than one on the same site keeps a single attribution record per visitor. The Vercel log drain records server request logs and needs the browser snippet alongside it for visitor sessions and conversions.

Browser snippet. Works on any site. Add the script tag shown on the Tracking page before the closing </body> tag on every page, or through your tag manager:

<script async
  src="https://surfacedby.com/t.js"
  data-site="YOUR_SITE_ID"></script>

The snippet records AI visitors and provides window.sbAi for reporting conversions. It does not record AI crawlers, which do not run JavaScript.

WordPress plugin. Install SurfacedBy AI Visibility from WordPress.org or download it from the Tracking page, then copy the private connection code from the WordPress install option and paste it under SurfacedBy > Connect in wp-admin. Older public Site ID connections must reconnect with a code to enable crawler verification. See WordPress Plugin for setup, and below for the stores it reports conversions from.

Cloudflare edge tracker. A Cloudflare Worker that runs in front of your site, so it also sees cached and static pages. The Tracking page walks you through it:

When Connect Cloudflare is available, authorize access, select the matching zone if asked, and follow the installation progress. The advanced API-token option uses a scoped token for that operation and discards it; automatic updates require an OAuth connection, and later removal needs fresh authorization for the same zone. Keep the token private. Manual setup remains available:

  1. Generate the collector secret.
  2. Create a worker in Cloudflare and paste the worker code. Your Site ID is already filled in.
  3. Add the collector secret to the worker as an encrypted secret named SURFACEDBY_COLLECTOR_SECRET.
  4. Add a worker route for your domain. Use a route rather than a custom domain, so the worker runs in front of your existing site.

Optional variables:

  • SURFACEDBY_COOKIE_DOMAIN shares the cookie across subdomains, for example example.com when visitors move between www.example.com and shop.example.com. Leave it unset to keep the cookie on one host.
  • SURFACEDBY_CONSENT_COOKIE names a first-party consent cookie that stores JSON with an analytics true or false value. Without an analytics grant, the worker does not set sb_attr and does not keep a visitor ID, but it still counts anonymous AI visits and crawls. In the EEA, the UK, and Switzerland, a visitor with no stored choice is treated as not consenting.
  • SURFACEDBY_INGEST_BASE and SURFACEDBY_HEARTBEAT_SECONDS rarely need changing.

NGINX server tracker. For sites served by your own NGINX, when you can run commands as root on that server. If you are unsure, use Cloudflare instead.

  1. On the Tracking page, open the NGINX tab and generate the collector secret.

  2. Click Get install command and run the command on the server that serves your site:

    curl -fsSL https://api.surfacedby.com/api/v1/tracker/nginx/install.sh | sudo sh -s -- --token <one-time token> --domain <your domain>

    The token works once within 30 minutes, so your signing secret never appears in a command or your shell history. The installer checks that NGINX passes nginx -t and has the njs module (version 0.8.1 or newer; if it is missing, the installer prints the package command for your system and stops). It then downloads and verifies the tracker, adds include lines to your site's configuration, tests and reloads NGINX, and waits for the first heartbeat. It backs up every file it changes and restores them if anything fails.

  3. Update later with the same command and --update in place of --token ... --domain .... --uninstall removes everything the installer added.

The tracker must run on the NGINX that receives your visitors. A cache inside the same NGINX (fastcgi_cache, proxy_cache) is fine. A cache or CDN in front of it (Varnish, a CDN) answers cached pages before NGINX sees them, so those visits are not counted; behind Cloudflare, use the Cloudflare tracker.

To configure NGINX by hand instead, open Configure NGINX by hand instead on the NGINX tab: it lists the files, the secret file command and the directives with your Site ID filled in.

Optional variables for a manual configuration: $surfacedby_cookie_domain (the same as the Cloudflare cookie domain), $surfacedby_ingest_base, $surfacedby_secret_path, $surfacedby_heartbeat_seconds, $surfacedby_registry_cache_path, and $surfacedby_heartbeat_state_path.

Edge tracker in front of WordPress. When the Cloudflare or NGINX tracker runs in front of a site that also uses the WordPress plugin, the edge tracker passes its visitor ID to WordPress in an X-SB-Client-Id request header. The plugin uses it when the visitor has no cookie yet, so both layers record the same visitor. The Cloudflare worker does this automatically. The NGINX installer adds the header in each location that already sets its own proxy headers or FastCGI parameters, so nothing your configuration inherits is lost, and the manual directives include it.

Safari. Safari can shorten the lifetime of cookies set by some server responses. The browser snippet keeps a copy of the attribution in the browser's local storage and restores the cookie from it. If you use an edge tracker and get a lot of Safari traffic, running the browser snippet as well keeps those visitors linked to their later conversions.

Vercel log drain. In Manage installation, choose Vercel log drain, enter the project ID, and select its environment. Generate the setup, then add a Logs drain in the team's Vercel settings for that project and environment. Use the HTTP endpoint and credentials shown in the drawer, with NDJSON and no compression. Run the Vercel test before saving, then check the last delivery in SurfacedBy.

Keep the generated endpoint and authentication values private. Rotating the setup invalidates the old endpoint; replace the drain configuration in Vercel too. Removing it in SurfacedBy stops acceptance, and you should remove the drain in Vercel. Logs with hidden client addresses cannot establish crawler identity. This method does not measure anonymous page loads or agent response readiness.

Report conversions

A tracker records the AI visit. The conversion itself comes from one of three places:

  • the WordPress plugin, for supported stores
  • the browser snippet, with window.sbAi.track()
  • the server webhook, for sales that happen on your server

If the same sale arrives from more than one of these, send the same order_id from each. SurfacedBy counts it once.

While no conversion source is connected, Set up conversion tracking on the AI conversions card opens the Conversion tracking setup drawer, with the code for each option. The server webhook is also available under Manage installation.

WordPress stores

With the plugin connected, conversions are reported automatically from:

Store pluginEvents reported
WooCommercepurchase, refund, and renewal (renewals need WooCommerce Subscriptions)
MemberPresspurchase, renewal, refund
Easy Digital Downloadspurchase, refund

MemberPress free trials are reported as purchases, and the first paid charge after a MemberPress free trial as a renewal. None of the built-in integrations report signups. Purchases are sent with an order_id of wc_ plus the WooCommerce order number, edd_ plus the Easy Digital Downloads order ID, or mepr_purchase_ plus the MemberPress transaction number; use the same value if you also report those sales another way. The plugin's Conversions tab appears while one of these store plugins is active, and the Track conversions setting is on by default. Developers can register their own integration with the surfacedby_aiv_conversion_adapters filter.

From the browser

With the browser snippet on the page, call window.sbAi.track() when a conversion completes, for example on your order-received page:

<script>
  if (window.sbAi && typeof window.sbAi.track === 'function') {
    window.sbAi.track('purchase', {
      event_id: 'ORDER-12345',  // your order number
      value: 49.99,             // net amount charged
      currency: 'USD',
      metadata: { plan: 'annual' }
    });
  }
</script>
  • event_id is required: 1 to 128 characters from letters, numbers, ., _, :, and -. A call with a missing or invalid ID is ignored. Sending the same ID again does not create a second conversion.
  • value defaults to 0. Values above 1,000,000 or below -1,000,000 are recorded as 0.
  • currency is a three-letter code and defaults to USD.
  • order_id is optional, up to 128 characters, and is used to match the same sale reported from another source.
  • metadata is an optional object. If it is larger than 4 KB it is dropped entirely.

The snippet also provides shortcuts: window.sbAi.purchase({...}), .trial(), .renewal(), .refund(), .signup(), .lead(), and .event(type, {...}). window.sbAi.getAttributionToken() returns the current sb_attr value, or null, for passing to your server.

If track() runs for a visitor who has no attribution cookie yet, the snippet creates one without an AI platform, so the conversion is counted under other channels.

Report conversions from your server

Use the Server webhook when a sale happens on your server and never passes through a page with the tracker, for example a payment processor webhook, a headless cart, or a CRM. It adds to a tracker install; it does not replace one, because the webhook cannot see visits. You find it under the optional Server-side conversions section of the install options and of the Conversion tracking setup drawer.

Send each conversion as a signed POST:

curl -X POST "https://api.surfacedby.com/api/v1/tracker/webhook/conversions" \
  -H "Content-Type: application/json" \
  -H "X-SurfacedBy-Site-ID: YOUR_SITE_ID" \
  -H "X-SurfacedBy-Timestamp: <ts>" \
  -H "X-SurfacedBy-Signature: t=<ts>,v1=<sig>" \
  -d '{
    "event_type": "purchase",
    "event_id": "ORDER-12345",
    "order_id": "ORDER-12345",
    "value": 49.99,
    "currency": "USD",
    "path": "/checkout/order-received",
    "occurred_at": "2026-05-18T18:00:00Z",
    "sb_attr": "<value forwarded from your checkout>"
  }'

Signing. Reveal the webhook secret in the Send webhook tab. Sign each request with HMAC-SHA256, using the secret as the key and <unix_seconds>.<raw_request_body> as the input, and send the lowercase hex result in X-SurfacedBy-Signature as t=<unix_seconds>,v1=<signature>. Send the same timestamp in X-SurfacedBy-Timestamp. Requests more than 5 minutes off the server clock are rejected, and every authentication failure returns 401. Rotating the secret stops the old one working immediately.

Fields. event_type is required, and so is either event_id or order_id: event_id falls back to order_id when omitted. The same rules for event_id, value, currency, and metadata apply as in the browser. A request that passes authentication but carries an unknown event type or no valid ID returns 200 with "accepted": 0 and records nothing.

Forward the attribution. The webhook runs on your server, so it cannot read the visitor's cookie. Without sb_attr in the payload, the conversion is recorded with no AI source. Read the sb_attr cookie (or call window.sbAi.getAttributionToken()) when the checkout starts, store it with the order, and include it unchanged in the webhook. The Forward attribution tab has examples for a hosted checkout that keeps it in session metadata, for Shopify cart attributes, and for a custom order system. When many recent webhook conversions arrive without it, the webhook card shows a warning.

Send a test. Send test conversion in the Send webhook tab sends a test purchase with a value of 0 through the same path as a real webhook, with an attribution token that credits it to ChatGPT, the way your server forwards the visitor's sb_attr cookie. It needs the webhook secret to exist. The result says whether the test was credited; a credited test shows the whole chain works, from receiving the sale to crediting the AI platform. Test conversions are left out of every total and appear in the recent conversions table with a Test label.

Event types

TypeUse it forValue
trialA free or paid trial starts0 for a free trial, the amount charged for a paid trial
purchaseThe first real payment, from a trial or a direct purchaseAmount charged
renewalA later recurring charge on an existing subscriptionAmount charged
refundA refund of an earlier orderNegative amount, for example -29
signupA new account or other unpaid signupUsually 0
leadA qualified form submission or other pre-sale conversionOptional
customAnything else you want to creditOptional

Report the subscription state rather than guessing from the amount. A paid trial is still a trial, and the first real charge after a trial is a purchase, even though many billing systems label it a renewal. Attributed Revenue adds up the value of every AI-credited event except trials, so it is net of refunds. A refund lowers revenue but is not counted as a conversion. Trials are counted separately and never as revenue.

Reported currencies

Revenue keeps the currency reported with each conversion. Amounts in different currencies are shown separately and are never added into one total or converted using an assumed exchange rate. Historical amounts without a usable currency remain labeled Currency not reported. A combined revenue value, average order value or revenue change is unavailable when its calculation would mix currencies or include an unknown unit. Conversion counts remain available independently of those amounts.

CSV exports keep separate revenue columns or rows for each reported currency, including amounts whose currency was not reported. A successful read with no conversions shows zero; loading or a failed read does not establish zero. Retry a failed read, and treat any retained previous result as a saved observation until its refresh succeeds.

What the Tracking page shows

  • AI Conversions and Attributed Revenue tiles at the top of the page, for the selected window (last 24 hours, 7 days, or 30 days).
  • The AI conversions card:
    • how many of all conversions in the window came from an AI platform, next to the count from other channels
    • By AI source: the top AI platforms, in the selected window
    • By event type: counts and revenue in the selected window
    • Avg order value: net revenue divided by conversion count
    • a bounded table of recent AI-credited conversions from the selected window, with the event, time, AI platform, value, and page
  • The Trials card, which lists trial starts from AI visitors when there are any.

Until conversions arrive, the card explains what is missing: no conversion source connected (with Set up conversion tracking), a source connected but no conversions yet, or conversions arriving that none came from an AI platform.

Conversion journey

Select a row in the recent conversions table to open Touches before the sale. It lists the visitor's AI visits in the 30 days before the conversion, up to 50, in order, and shows which ones earned credit. A conversion with no visitor ID, or with no AI visits in those 30 days, shows why no journey is available.

Attribution models

The journey view has four ways to split credit across the AI visits in that 30-day window. Your choice is saved per domain.

ModelHow credit is split
Last touchAll credit to the most recent AI visit. This matches the headline numbers.
LinearAn equal share for every AI visit.
Time-decay (7d)More credit to recent visits. A visit's weight halves for every 7 days before the conversion.
Position-based (40/20/40)40% to the first visit, 40% to the last, and 20% shared across the visits in between. With two visits, each gets 50%; with one, it gets 100%.

Privacy and data

  • Visitor IDs are anonymous. The sb_attr cookie holds a random visitor ID, the AI platform, the time of the first AI visit, and any ad click IDs.
  • IP addresses are not stored. They are hashed with a salt that changes daily and used only for rate limiting. For AI crawlers, the address is used to verify the crawler and then discarded. The browser snippet does not collect IP addresses.
  • Conversions store the event type, event ID and order ID, value, currency, time, page, the anonymous visitor ID, the AI platform credited, and any metadata you send. Do not put personal information in metadata.
  • AI visit history used to build conversion journeys is kept for 90 days on Starter and trials, 395 days on Professional, and 760 days on Agency. Journeys only read the 30 days before each conversion.

On this page