Skip to main content

Web SDK and the public-key identify/track model

Orbit exposes a second public surface next to the signed pixels and redirects: the web personalization SDK endpoints, mounted at /sdk/* on the root host — deliberately outside the /api/v1 version prefix the authenticated API uses. The @devotel-orbit/web browser SDK calls them from your site’s JavaScript with your tenant’s public API key (dv_live_pk_…), so visitors can be identified and their behavior tracked without any server round-trip of your own. This page explains why the public key is safe to ship in a browser bundle, how /sdk/identify reconciles an anonymous visitor into a known contact, and where the security envelope differs from the public pixels and redirects family.

Two public surfaces, two different contracts

Orbit has exactly two bands of endpoints reachable without a secret credential, and they solve different problems: A pixel URL is self-authenticating: the signature travels inside the URL, so no key is needed and there is nothing to revoke. The SDK surface does real writes — creating and updating contacts, recording behavior — so it needs to know which tenant it is writing for. That is what the public key resolves: one hashed lookup binds the request to exactly one tenant schema before any body is parsed. Both families are fail-closed on auth and fail-neutral on response shape; their abuse defenses are per-IP rate limits, shared with the rest of the public band. For the HMAC-signed families, see Public pixels, redirects, and short links.

Why a public key (pk_) is safe to ship in browser JS

The dv_live_pk_ key is designed to be visible. It will appear in the source of every page that embeds the SDK, and that is fine, for four reasons:
  1. Read-only scopes. Public keys are created with read scopes only; the platform rejects a write or administrative scope on a pk at creation. A leaked public key can never reach the messaging, billing, or admin surfaces that a secret key (dv_live_sk_) unlocks. See Authentication and session model.
  2. Writes confined to a narrow band. The only write endpoints a public key can reach are POST /sdk/identify, POST /sdk/track, and POST /sdk/in-app/events — and all three write into the key’s own tenant schema, nothing else. A leaked pk lets someone push behavior events or identification calls into your own audience data, which is spam-class abuse bounded by rate limits (below), not a credential for reading or mutating your account.
  3. Hash-stored, revocation-checked. The platform stores sha256(key), never the plaintext, and every request re-checks that the key is active, unrevoked, unexpired, and — critically — of type public. A secret key with an altered prefix cannot authenticate the SDK surface; a key belonging to a deleted organization stops resolving immediately.
  4. No version prefix in the URL. The /sdk/* routes mount on the root host, not under /api/v1, so your embed URL is stable across API versions: https://api.orbit.devotel.io/sdk/track never needs re-embedding when /api/v2 arrives. Pixel and redirect URLs live at the root for the same reason — the path a customer site embeds is a contract, and version churn belongs to the authenticated API.

Stitching: how identify reconciles an anonymous visitor

Every SDK session starts anonymous. The SDK mints an anonymous_id and tracks events under that id alone — no contact exists yet, so rows land with a null contact and the anonymous id as the only join key. Stitching happens when the site learns who the visitor is — a login, a checkout form, a phone or email capture — and calls POST /sdk/identify with that strong identifier:
What the call does, in order:
  1. Resolve or create the contact. Phone is the preferred arbiter (a phone can unlock messaging and voice); email is the fallback. An existing live contact with the identifier is updated in place; otherwise a new contact row is created. Idempotent by consequence: re-calling with the same pair returns the same contact.
  2. Backfill the pre-identify funnel. Every event row captured under this anonymous_id gets its contact stamped, so the contact timeline in the dashboard shows the full journey the person actually traveled — anonymous browsing included — not just “first seen after they logged in.”
  3. Record the surfaces the stitch unlocks. Web tracking is the immediate backfill. Messaging, voice, and video self-identify on the same phone or email the next time the person arrives through those channels — no further stitching call is needed, because each inbound channel keys resolution on the identifier it carries.
When the anonymous session and the known person already sit on two separate contact rows (the browser stitched late to a contact an inbound call created earlier), the browser-side stitch stops at attribution — the operator-side merge is the CDP identity resolution surface, which collapses the two rows under audit.

In-app events vs pixel events

POST /sdk/in-app/events ingests engagement receipts for the in-app message and content-card surfaces the SDK renders — one call per impression, click, or dismiss. Three properties distinguish it from both /sdk/track and the pixels:
  • It is anonymous by design: the payload carries an anonymous_id (plus your optional host-app user id) and lands with a null contact, then rides the same identify backfill as track events.
  • It writes to the same analytics sink as /sdk/track, so card engagement appears on the contact timeline next to site behavior rather than in a silo.
  • Unlike a pixel, it is not attribution against a message send — an in-app surface is an owned channel with no carrier leg. “Delivery” is the SDK pulling the feed; the events endpoint only reports what the visitor did with it. See In-app channel model.

Security envelope

The SDK band shares the perimeter posture of the pixels but replaces URL-signing with key resolution:
  • CORS is global. The shared application plugin answers cross-origin requests from arbitrary origins — your visitors’ browsers call from your domain, not Orbit’s, so an origin allow-list would break the model. Origin is therefore not an authentication signal here; the key is.
  • Per-IP rate limits. Both the public-keyed endpoints and the anonymous beacon fallback carry their own per-IP budgets, so a leaked pk degrades to bounded event-spam, not a flood.
  • No PKCE, no Clerk. Those belong to dashboard sessions and OAuth flows. Browser embeds never perform a login handshake; identity arrives only through the explicit /sdk/identify call your site chooses to make.
  • Payload discipline. Event property payloads are size-bounded and the whole body is schema-validated before a single write, so malformed or bloated batches get a 422, not a database row.
The anonymous beacon fallback (POST /sdk/track/anonymous) deserves a note: old browsers that cannot keep a request alive across page unload send events with no key at all into a staging holding area tied to nothing. A reconcile pass attributes staged rows to a tenant only after a later identify call binds the same anonymous_id — until then no tenant association exists, so there is nothing to leak cross-tenant. Modern browsers carry the unload flush with the keyed fetch path and never touch the fallback.

Common pitfalls

  • Shipping a secret key in the bundle. A dv_live_sk_ key in client-side JavaScript hands account-level access to anyone who views source. Use the public key for all browser surfaces; secret keys stay on your server.
  • Identifying on every page load. Identify once per session, when a strong identifier appears. Re-identification is idempotent, but it is wasted bandwidth; the backfill is a one-way stamp.
  • Expecting stitching to merge two contact rows. Identify attributes the anonymous funnel to a resolved contact. When an operator later discovers the person duplicated across two rows, that consolidation belongs to the identity-resolution merge, under audit — the browser surface never collapses rows.
  • Reading “no contact yet” as “no data yet”. Pre-identify events are stored with the anonymous id alone and get attached on identify. Reports filtered to known contacts simply don’t show the anonymous tail until stitching happens.