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
Thedv_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:
- Read-only scopes. Public keys are created with read scopes only; the platform rejects a write or administrative scope on a
pkat 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. - Writes confined to a narrow band. The only write endpoints a public key can reach are
POST /sdk/identify,POST /sdk/track, andPOST /sdk/in-app/events— and all three write into the key’s own tenant schema, nothing else. A leakedpklets 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. - 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 typepublic. A secret key with an altered prefix cannot authenticate the SDK surface; a key belonging to a deleted organization stops resolving immediately. - 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/tracknever needs re-embedding when/api/v2arrives. 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 ananonymous_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:
- 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.
- Backfill the pre-identify funnel. Every event row captured under this
anonymous_idgets 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.” - 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.
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
pkdegrades 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/identifycall 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.
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.
Cross-links
- Public pixels, redirects, and short links — the HMAC-signed public band this surface complements.
- Authentication and session model — public vs secret key forms and how resolution is decided.
- CDP event model — how tracked events flow into profiles, segments, and activation.
- CDP identity resolution — the operator-side merge for contacts already split across rows.
- In-app channel model — the owned channel behind
/sdk/in-app/feedand its engagement events. - Web SDK — installation, modules, and usage of
@devotel-orbit/web.