Skip to main content

Session Handoff

Definition​

This document explains how a web page running Pulse continues the session of another Pulse platform when the user arrives through an OAuth redirect, and how the producing platform builds the value that carries the session.

The session travels in the OAuth state query parameter. When the landing page loads with a valid, fresh handoff, Pulse adopts the producer's session_id instead of keeping or starting a local one, so both platforms report the user's journey as a single session.

The state parameter​

The landing URL carries:

?state=<nonce>.<base64url(returnPath)>.<base64url("psid=<lastActivityMs>|<sessionId>")>
  • The value has 3 or more dot-separated segments. Pulse reads only the third one; the nonce, the return path and any later segments are left to the OAuth flow.
  • The third segment is the base64url encoding of a query string with a psid key. Other keys in it are ignored.
  • lastActivityMs is the producer's last activity time, in milliseconds since the epoch.
  • sessionId is the producer's current Pulse session_id.

Pulse reads the URL the page first loaded with, so the handoff still works when the application removes state from the address bar before Pulse boots.

When the session is adopted​

A handoff is adopted when its lastActivityMs is fresh:

  • less than the session timeout (30 minutes) in the past, and
  • at most 60 seconds in the future, to tolerate a producer clock that runs ahead of this device.

The rule depends only on the timestamp. There is no apply-once marker, so the same fresh URL is adopted again on a reload, on the Back button (including a page restored from the back-forward cache) and when the URL is pasted into a new tab. Once lastActivityMs is stale, the URL is ignored and the page keeps its normal session.

A handoff is ignored, with no side effects, when:

  • it is stale, or more than 60 seconds in the future;
  • the state value is missing or malformed in any way (see Validation rules);
  • the page is a Web To Mobile page with a session_id in w2m_meta_info — the Web To Mobile session always wins;
  • the tracker runs in the anonymous or page-session mode, which keep the session in memory.

What happens on adoption​

  • The adopted id becomes the session id for every tab of the site, and the last activity is set to the time of adoption. From then on the session expires, renews and syncs across tabs as usual.
  • session_start fires only when the adopted id differs from the stored session id. It has no source, like a first session. When the stored id already equals the handoff id — a reload, a Back or a pasted URL of the same handoff — the session resumes silently.
  • A page with a fresh handoff never rotates the session because of its own referrer, UTM parameters or click id. The OAuth provider is usually an external referrer, which would otherwise start a new session.
  • On a page restored from the back-forward cache, the handoff is checked again with the same rules. A session_start fired there is not preceded by a navigation event, because Pulse sends no navigation on a restore.
caution

The adopted session_id embeds the producer's device id (session ids have the form {startMs}_{deviceId}), while device_id stays the local one. Never derive device_id from session_id.

note

Only SDK bundles that include the handoff read state. A browser still running an older cached bundle ignores it and keeps a local session.

Producer guide​

The platform that starts the OAuth flow builds the third segment from its own Pulse state, right before redirecting:

const { session_id } = await pulse.getState();
const psid = `psid=${Date.now()}|${session_id}`;

const toBase64Url = (value) =>
btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");

const state = [nonce, toBase64Url(returnPath), toBase64Url(psid)].join(".");

Build the value as late as possible: Date.now() is the activity time the landing page checks for freshness, and the user may take several minutes on the OAuth provider's screens.

Validation rules​

The landing page accepts the handoff only when all of these hold. Anything else is ignored silently.

PartRule
stateAt most 2,048 characters, with 3 or more segments separated by .
Third segment1–512 characters of the base64url alphabet [A-Za-z0-9_-]. Up to two trailing = padding characters are allowed
Decoded segmentPrintable ASCII only, parsed as a query string with a psid key. The pipe separator may be raw or encoded as %7C
lastActivityMsExactly 13 digits; fresh as described in When the session is adopted
sessionId{startMs}_{device part}: startMs is exactly 13 digits and not later than lastActivityMs; the device part is 4–128 characters of [A-Za-z0-9._-]
tip

A standard Pulse session_id already has the required shape. Make sure the producer's tracker includes session_id in its state (the default in standard mode); otherwise getState() returns no session_id and there is nothing to hand off.