Guide/Multi-Domain Attribution

Multi-Domain Attribution

Track one visitor across a marketing domain and a separate application domain.

A visitor lands on a marketing site, then converts on a different domain that hosts the product. This guide explains what carries attribution across that boundary, what does not, and how to configure both ends.

The running example is a landing page on codelynx.dev and an application on codeline.app.

One tracking site or two

Start with one tracking site listing every hostname that sends events. A site accepts up to 20 hostnames, and the same publishable key works on all of them.

Create a second site only when you need separate settings, because separate settings are the only thing a second site buys you:

  • an independent publishable key, so one can be revoked without breaking the other,
  • a separate Active/Disabled switch,
  • a different attribution window,
  • a separate rate-limit budget, so a high-traffic domain cannot exhaust the quota of a quiet one.

Two sites do not split reporting. Events are not tagged with the tracking site that ingested them; clicks, customers, conversions, and revenue are always reported per link and per organization. Use separate links or UTM parameters when you need to tell two surfaces apart.

Only hostnames that send requests need to be listed. Capturing a click identifier and reading it back are purely local operations, so a marketing page that never calls identify or track does not have to appear in the allowed origins.

What crosses a domain boundary

The click identifier arrives on the destination as ?lf_id=... and the SDK stores it in first-party localStorage. That storage belongs to one origin: codelynx.dev cannot read what codeline.app wrote, and the reverse is also true. This is a browser rule, not a LinkQuick setting.

A customer, on the other hand, is keyed by your organization and your own external customer identifier. Once a click has been bound to that identifier by identify, every later event for the same identifier inherits the attribution from the customer record, whichever domain the event came from.

So the bridge between two domains is your user ID, not browser storage.

Case 1 — the visitor signs in on the first domain

click → codelynx.dev?lf_id=abc → identify("user_123")
                               → codeline.app  → track("plan.upgraded")

Nothing extra is required. The event sent from codeline.app carries no local click identifier, but LinkQuick resolves the customer user_123 and applies the click already recorded on the customer.

Case 2 — the visitor stays anonymous on the first domain

This is the common shape: the landing page has no accounts, and the visitor only signs up once they reach the application.

Here the click identifier is stranded in the landing page's storage. An identify call from the application would be rejected with Browser identity requires a current LinkQuick click ID, because public browser identity is only accepted when it proves possession of a real, eligible click.

Forward the identifier on outbound links so the application can capture it:

const linkquick = LinkQuick.create({
  endpoint: "https://YOUR-CONVEX-DEPLOYMENT.convex.site",
  publishableKey: "lq_pk_YOUR_PUBLISHABLE_KEY",
  attributionWindowDays: 30,
});

const clickId = linkquick.getClickId();
if (clickId) {
  for (const anchor of document.querySelectorAll('a[href*="codeline.app"]')) {
    const url = new URL(anchor.href, location.href);
    url.searchParams.set("lf_id", clickId);
    anchor.href = url.toString();
  }
}

On arrival the SDK captures lf_id, stores it under the application's own origin, and removes the parameter from the visible URL. The later identify call then has an eligible click and succeeds.

Re-run the decoration after any render that adds links, such as a client-side navigation or a component that mounts late.

Send the money from a server

Browser ingestion deliberately rejects conversion revenue. A purchase should be reported from your backend with an Admin API key, using the click identifier you persisted when the account was created:

curl -X POST https://linkquick.melvynx.dev/api/v1/events \
  -H "Authorization: Bearer $LINKQUICK_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
    "externalCustomerId": "user_123",
    "clickId": "CLICK_ID",
    "eventName": "subscription.created",
    "kind": "conversion",
    "revenueMinor": 9900,
    "currency": "EUR",
    "idempotencyKey": "subscription-sub_123-created"
  }'

The server path is also the only one that survives a payment webhook arriving before the customer returns to the browser. See Attribution and Events for the full API.

Verify a multi-domain setup

  1. Open an active short link pointing at the marketing domain in a fresh browser session.
  2. Confirm linkquick:attribution exists in that domain's local storage.
  3. Follow a decorated link to the application and confirm lf_id appears once in the address bar, then disappears.
  4. Confirm linkquick:attribution now also exists under the application's origin, holding the same identifier.
  5. Complete a sign-up and check the customer in Customers; the journey should start at the original click.

A forbidden_origin response means the calling hostname is missing from the tracking site. Hostnames match exactly: example.com, www.example.com, and app.example.com are three separate entries, and wildcards are not accepted.

Known limit

If the visitor leaves the marketing domain and reaches the application later by another route — a bookmark, a search result, a typed address — no lf_id travels with them and the click is lost. Attribution cannot be recovered without third-party cookies. LinkQuick keeps the customer and their events, but does not fabricate a link attribution.