Skip to main content
Version: v1

Tenant isolation

A merchant is a tenant. Each tenant has a single VoiceBot identity, but it is reached through three different credentials, one per plane (see Architecture). They are not interchangeable: each is scoped to its plane, flows in its own direction, and is verified differently. The invariant across all three: the client never asserts which tenant it is — the backend derives tenant_id from the credential and binds it server-side.

The three models at a glance

1 · Backend ingest2 · Frontend widget3 · Native mobile
DirectionYour server → VoiceBot (push)Browser ↔ VoiceBot (live)App ↔ VoiceBot (live)
CredentialHMAC shared secret (sk_-class, per tenant)Publishable key pk_…vb_pk_ (Mode B) or HMAC secret (Mode A) → session token
Where it livesYour server, encrypted at restShipped in the browser bundleShipped in the app binary
Secret?Yes — never logged or in a responseNo — public, origin-lockedNo — public; token is short-lived
Bound bytenant_id header + signature over the requestOrigin allow-listOrigin/app-id allow-list, token claim

1. Backend ingest — HMAC signed push

The server-to-server catalog feed is authenticated with HMAC-SHA256. Pairing mints a per-tenant shared secret (32 random bytes) which your server stores encrypted at rest and never logs or returns. Each request sends X-VoiceBot-Tenant-Id and a signature over METHOD\npath\nts\nnonce\nbody_sha256. Replay is blocked two ways: the timestamp must be within ±300s of server time, and the nonce is single-use for 600s per tenant. Because the nonce is consumed even on an error, signed requests are not blindly retried. Full detail in Auth & HMAC.

2. Frontend widget — origin-locked publishable key

The browser widget authenticates with a publishable key pk_… — a public, origin-locked identifier (like a Stripe pk_), safe to ship in markup. The widget self-mints: it exchanges the pk_ plus the browser-set Origin for a short-lived session token. The backend resolves the tenant from the pk_ and signs tenant_id into the token claim — the client never sends it. The security boundary is the origin allow-list (exact match, no wildcards), checked at the token mint (403 on miss) and again at the WebSocket upgrade (4403 on miss).

3. Native mobile — WebSocket session token

A native app connects with a short-lived session token, then opens the voice/text WebSocket with it. As on the web, the token carries the tenant claim; the device never asserts a tenant_id. There are two ways to mint that token: Mode A (recommended) — your backend, holding the same HMAC shared secret it uses for ingest, server-mints the token, so no key ships in the binary; Mode B — apps without a backend embed a publishable key and self-mint. The mobile publishable key is prefixed vb_pk_ (distinct from the web widget's pk_ — don't conflate them); it is an identifier, not a secret, bound to your app bundle id / package, and safe in the app binary. See the mobile auth guide.

What the merchant is responsible for

VoiceBot enforces the boundaries, but two things are yours to manage:

  • Keys and secrets. Keep the HMAC shared secret server-side and encrypted; rotate it by re-pairing if it could have leaked. The pk_ is public by design — you don't protect it, you constrain it with origins.
  • Origins and app ids. The allow-list is the security boundary for the public-key planes. List every exact origin (including www, subdomains, and dev ports) and every app bundle id. A site or app not on the list is denied — that is the intended behaviour, not a bug.
One identity, do not cross the streams

The shared secret signs ingest and never appears in a browser or app. The pk_ authorizes the widget/app and can never sign ingest. Pairing a backend does not expose or replace your public key, and vice versa.