Skip to main content
Version: next

Auth & API keys

Authentication mirrors the web widget exactly. This page is the same for both SDKs — only the init snippets differ. (For the product-wide map of every credential — pk_, vb_pk_, the ingest secret — see API keys.)

The VoiceBot key (one key, both surfaces)

A tenant has one VoiceBot key = a paired tenant_id plus a shared secret, created when you pair the plugin (stored AES-GCM on the backend in provider_connections). The same key authorizes both your website widget (origin-bound) and your mobile app (bundle-id-bound). You do not provision a separate mobile credential.

The SDK never holds the shared secret in the app binary. It obtains a short-lived session token (a JWT) and connects with that. There are two ways to get the session token — pick one via the SDK's TokenProvider abstraction.

┌─────────────────────────── Mode A (recommended) ───────────────────┐
│ shared secret stays here │
┌──────────┐ │ ┌──────────────────┐ HMAC-signed ┌──────────────────────────┐ │
│ Your app │ ──────▶│ │ Your backend │ ─────────────▶ │ POST /api/v1/mobile/ │ │
│ + SDK │ token │ │ (holds secret) │ issue-token │ issue-token │ │
└──────────┘ req │ └──────────────────┘ ◀───── JWT ─── └──────────────────────────┘ │
▲ └────────────────────────────────────────────────────────────────────┘

│ ┌──────────────────────────── Mode B (no backend) ──────────────────────────────┐
└───│ SDK ── vb_pk_ + app_id ─▶ POST /api/v1/mobile/issue-token-public ── JWT ─▶ SDK │
└────────────────────────────────────────────────────────────────────────────────┘

Your backend (the one already serving the app its products and categories) holds the shared secret and mints the session token, exactly like the web widget. The secret never ships in the binary — this is true web-parity and the recommended path.

Your backend signs POST /api/v1/mobile/issue-token with the ingest HMAC:

POST /api/v1/mobile/issue-token
X-VoiceBot-Tenant-Id: <uuid>
X-VoiceBot-Timestamp: <epoch seconds> # ±300s window
X-VoiceBot-Nonce: <single-use, 600s TTL>
X-VoiceBot-Signature: <hex>
X-VoiceBot-App-Id: <ios bundle id / android package>

{ "lang": "uk" }
200 OK
{ "session_token": "<JWT>", "expires_at": <epoch>, "refresh_after": <epoch> }

The signature preimage (exact — do not guess)

The signature is an HMAC-SHA256 over a newline-joined canonical string. This is the exact preimage the backend verifies; an earlier draft guessed "<ts>.<nonce>.<body>" — that is wrong.

sig = HMAC_SHA256(secret, "{METHOD}\n{path}\n{timestamp}\n{nonce}\n{sha256_hex(raw_body)}").hex()

# e.g.
# "POST\n/api/v1/mobile/issue-token\n1718000000\n<nonce>\n<sha256hex(body)>"
#
# body_sha256 = sha256(raw_request_body_bytes).hexdigest()
# empty body → sha256(b"") hex

The app fetches the resulting session_token from your backend. Wire it up via a TokenProvider:

const client = VoicebotClient.init({
baseUrl: 'https://api.monoverse.tech',
auth: {
mode: 'custom',
tokenProvider: {
async issueToken(lang) {
const r = await fetch('https://shop.example.com/voicebot/token', {
method: 'POST',
body: JSON.stringify({ lang }),
});
return r.json(); // { sessionToken, expiresAt, refreshAfter }
},
},
},
});

The SDK also ships an HmacTokenProvider that performs the signing itself. Use it only where holding the shared secret is acceptable (a trusted server runtime or local development) — never in a shipped binary.

Mode B — publishable key (no backend)

For apps without a backend. Embed a publishable key vb_pk_… — an identifier, not the secret, safe to ship (like a Stripe pk_). Get it at Cabinet → Integrations → Mobile app.

The SDK calls the public endpoint:

POST /api/v1/mobile/issue-token-public
{
"api_key": "vb_pk_…",
"app_id": "<bundle/package>",
"attestation": "<App Attest / Play Integrity, optional>",
"lang": "uk"
}
200 OK { "session_token": "<JWT>", "expires_at": <epoch>, "refresh_after": <epoch> }
4401 key inactive
4403 app_id not in allowlist
const client = VoicebotClient.init({
baseUrl: 'https://api.monoverse.tech',
auth: { mode: 'publishableKey', apiKey: 'vb_pk_…', appId: 'com.yourco.app' },
});

Why a public key is safe

The security boundary for Mode B is not the secrecy of the key — it is the app_id allowlist + the connection status. An attacker who pulls your vb_pk_ out of your APK and calls the public endpoint with a different app_id gets 4403 — no token. With an app_id that is on your allowlist (i.e. they repackaged your own bundle id) they get a token for your tenant's public bot — which is exactly what the key is for. Cross-tenant access is structurally impossible: tenant_id is resolved server-side from the key and placed in the signed JWT claim; it is never taken from a client parameter.

The session token

session_token is an HS256 JWT (same mechanism as the web widget) with claims including tenant_id, shop_id, app_id, language, channel: "mobile", and allowed_app_ids (which replaces the web token's allowed_origins).

  • TTL ≈ 3600s, refresh-after ≈ 1800s. The SDK re-issues before refresh_after.
  • Stored in the Keychain (iOS) / EncryptedSharedPreferences (Android)never logged.

Billing kill-switch (revoke)

The backend mints tokens only while the connection is active (provider_connections.status == "connected"). This is the "account alive → key works" lever:

  • Flip the status (subscription lapsed, over a limit) → every token request fails and active sessions are revoked within ~30s (close code 4401).
  • No extra wiring on your side — it reuses the same status the web widget is gated on.

Attestation (optional hardening)

App Attest (iOS) / Play Integrity (Android) is optional hardening layered on Mode B, configurable per-connection (attestation_required). It is not the primary gate — the primary gate is key + app_id allowlist + connection status (web parity). Enable it for tenants who want stricter repackage/emulator resistance.

Transport security

  • WSS only. The SDK rejects ws:// (except localhost for development).
  • Certificate pinning is opt-in and rotation-aware.

See also