Documentation menu

Cross-domain journeys

Continue an anonymous visit across your own domains while keeping its session and basic first/last-touch attribution. Configure Settings → Data → Cross-domain journeys separately for each environment.

  1. Enable linking and list two to twenty exact HTTPS origins, such as https://www.example.com and https://app.example.com. No paths or wildcards. Loopback HTTP origins are supported for local development.
  2. Also include both origins in your application's allowed ingest origins, if that list is restricted.
  3. Install the browser SDK on each domain with public keys from the same application and environment. Apply your consent choice independently on each domain.
import { spm } from "@saaspro/browser";
spm.init({
  key: "spm_pub_prod_xxxxxxxx",
  host: "https://saaspro.dev",
  privacyMode: "persistent",
  consent: "pending",
  crossDomain: {
    origins: ["https://www.example.com", "https://app.example.com"],
  },
});
// Call from your consent controls after the visitor permits collection.
spm.consent("granted");

React's provider accepts the same config. For the script tag, use data-cross-domain-origins="https://www.example.com,https://app.example.com" alongside data-key, data-host and your consent attributes.

Ordinary same-tab links are handled automatically. Downloads, modified clicks and links targeting another window keep their normal behavior. For custom navigation, set autoLink: false (script: data-cross-domain-auto-link="false") and use:

location.assign(await spm.createLink("https://app.example.com/start"));

Link creation flushes source events first. At least one recent anonymous source observation must be retained. A failed or slow handoff returns the original URL within two seconds (or a shorter configured request timeout). If you disable automatic pageviews, explicitly capture a source event before creating a link.

What crosses the domain

The URL receives an opaque, single-use spm_handoff fragment component. It expires after 60 seconds and is stripped before SDK capture. Other fragment components remain intact. The destination waits for consent and ticket restoration before its first capture; an unavailable ticket starts a normal visit. Calls made during restoration are held in a bounded, 100-call initialization queue.

Only the source browser's submitted anonymous ID, session timing and basic attribution transfer. First/last-touch source, medium and campaign survive; landing/referrer queries and fragments are removed. Own-domain referrers do not become new acquisition sources. Ad click identifiers, flags, account identities, traits and historical data do not transfer. Existing identified visitors retain their identity. No historical person merge occurs.

Pending/revoked consent, opt-out and short-lived (ephemeral) collection prevent linking. A ticket waiting for destination consent can expire. Ticket consumption confirms transfer, not successful subsequent ingestion; limits or sampling may still discard destination events.

Verify

Start a fresh anonymous visit on the source with UTM parameters, allow collection, then follow a configured link. On the destination, allow collection promptly. Compare spm.getAnonymousId(), spm.getSessionId() and spm.getAttribution() before and after the hop. Confirm destination events in Analytics → Live, then inspect Journey activity for issued and consumed counts.

If linking fails, check the selected environment, both origin lists, module enablement, retained source events and consent on both domains. Every settings save invalidates outstanding tickets. Management API clients can read or update /api/v1/apps/{org}/{app}/journeys?env=production using the current expectedRevision; reads expose only policy and aggregate counts.

Transport and retention

POST /api/v1/ingest/journey accepts a bounded handoff event with a public browser key. POST /api/v1/journeys/redeem additionally requires the secret, single-use ticket capability and its exact destination Origin. The key alone cannot read visitor state. Tickets bind one application/environment and transfer only caller-submitted state; they are not authentication tokens for your application. Avoid logging decorated URLs in your own client instrumentation.

Each environment permits up to 1,000 outstanding tickets and 10,000 recent receipts, with shared ingest rate limits. State is inaccessible after one minute and cleared on consumption. A minute worker clears expired payloads and deletes receipts after fifteen minutes; physical cleanup can lag if the worker is unavailable. Daily aggregate activity is retained for ninety days.