Skip to main content
Version: v1

Installation & pairing

From zero to a paired connection. You will not push any data here — that happens in Syncing.

Requirements

  • Python 3.11+.
  • A full-entropy 32-byte encryption_key — generate one with openssl rand -hex 32 and keep it in your secret manager.

1. Install

Coming soon

The Python SDK is not yet published to PyPIpip install monoverse-voicebot does not work yet. Until it ships, install from source.

Terminal — install from source
git clone https://github.com/alyokhin-n/monoverse-voicebot.git
pip install -e monoverse-voicebot/packages/voicebot-python

Depends on httpx, pydantic, and cryptography. Ships py.typed; mypy --strict clean.

2. Construct the client

voicebot.py
import os
from monoverse_voicebot import VoiceBotClient

client = VoiceBotClient(
base_url="https://api.monoverse.tech",
site_url="https://shop.example.com",
encryption_key=os.environ["VOICEBOT_ENCRYPTION_KEY"],
credentials_file="./.voicebot-credentials.enc", # omit for in-memory only
)

VoiceBotClient is a context manager — use with VoiceBotClient(...) as client: to close the underlying httpx.Client cleanly, or call client.close() yourself.

ArgumentPurpose
base_urlIngest base URL. Must be https:// (localhost allowed for dev).
site_urlSent as X-VoiceBot-Site-Url on every request; used as the default pairing site.
encryption_keyEncrypts the stored shared secret at rest (AES-256-GCM).
credentials_filePath for the encrypted credential envelope. Omit to keep credentials in memory only.
https only

base_url must be https://. Plaintext http:// is rejected (an HMAC over http would leak the signature to a network attacker). localhost / 127.0.0.1 / ::1 are the only exceptions (local dev).

3. Get a pair code

In your VoiceBot cabinet, generate a one-time pair code. It looks like VB-XXXX-XXXX.

4. Pair

pair.py
from voicebot import client

result = client.pair("VB-ABCD-1234")
print("paired tenant", result.tenant_id, "ingest", result.ingest_url)

pair() performs the unauthenticated handshake, mints your tenant id and shared secret, stores them encrypted, and returns a secret-free summary (tenant_id, ingest_url, optional account). The shared secret is never returned to the caller.

The shared secret is never exposed

pair() stores the secret encrypted (AES-256-GCM, keyed by your encryption_key) and the Credentials object redacts it from repr() and str(). There is no API that reveals it. If you lose the key, re-pair.

Already paired?

The client loads stored credentials on first use. To switch tenants, unpair() first, then pair() again.

How signing works

You normally never touch the signer — every authenticated method on VoiceBotClient signs for you. This is what happens on the wire, so you can debug a 401 bad_signature.

Every authenticated ingest request is signed with HMAC-SHA256 over a canonical string. The shared secret comes from pairing (32 random bytes, base64 in the /pair response — decode to raw bytes before signing). The string-to-sign is exactly five fields joined by \n:

canonical = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + SHA256_HEX(BODY)
signature = HEX( HMAC_SHA256(shared_secret_raw, canonical) )
  • METHOD — uppercase HTTP verb (POST, PUT, GET).
  • PATHrequest.url.path only: no host, no query string.
  • TIMESTAMP — the exact string sent in X-VoiceBot-Timestamp (Unix epoch seconds, UTC). Do not reformat it.
  • NONCE — the exact string sent in X-VoiceBot-Nonce (base64url of 16 random bytes). Do not decode it.
  • SHA256_HEX(BODY) — lowercase hex SHA-256 of the raw request body bytes you actually send. For an empty body this is the well-known constant e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

The bytes you hash must be byte-for-byte the bytes you transmit — re-serializing the JSON after signing changes the hash and the server rejects it.

Canonical string — POST /api/v1/ingest/init with an empty body
POST
/api/v1/ingest/init
1747396800
aGVsbG8td29ybGQtYTFiMmMz
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Off-by-one breaks the signature

A lowercased method, an extra trailing newline, a re-encoded nonce, or hashing a re-serialized body all produce a different signature and a 401 bad_signature. Build the canonical string from the exact header strings and the exact body bytes.

Replay protection. Each request also carries a fresh timestamp and nonce. The server accepts a timestamp within ±300s of its clock (stale_timestamp otherwise) and a nonce only once per 600s window per tenant (nonce_replay otherwise). Because the nonce is consumed even on an error, do not blindly retry a signed request — see retry semantics in your endpoint reference.

The request headers

Every authenticated request carries these headers. They are the inputs to the signature, the tenant binding, and the replay window.

HeaderValueNotes
X-VoiceBot-Tenant-IdUUIDReturned by /pair. Identifies the tenant whose secret signed the request.
X-VoiceBot-Timestampinteger (Unix epoch seconds, UTC)Replay window is ±300s of server time.
X-VoiceBot-Noncebase64url of 16 random bytesSingle-use; tracked for 600s per tenant. Fresh value per request.
X-VoiceBot-Signature64-char lowercase hexHMAC_SHA256 of the canonical string.
X-VoiceBot-Protocol-Version2Always the integer 2 on the wire. Required on every request including /pair.
X-VoiceBot-Plugin-Versionsemver stringProducer version, e.g. 2.1.0. Telemetry only.
X-VoiceBot-Site-UrlURLOrigin of the sending site. Heartbeat upserts the site row from it.
Content-Typeapplication/json or application/x-ndjsonPer endpoint; NDJSON upload may be gzipped.
Idempotency-KeyUUIDOptional on /events; required on full-snapshot /finalize. Response cached 24h.
Protocol-Version foot-gun

The wire header X-VoiceBot-Protocol-Version must always be the integer 2 — a mismatch is rejected with 426 Upgrade Required. The JSON body of the /pair response currently reports "protocol_version": 1, which is a server-side field value, not the version you send. Ignore the response field for signing purposes and keep sending 2 on the wire.

The Protocol-Version foot-gun

The client always sends X-VoiceBot-Protocol-Version: 2 (the PROTOCOL_VERSION constant). The /pair response body reports protocol_version: 1 — that field is stale; never read it for signing. Sending anything but 2 on the wire is rejected with 426 Upgrade Required.

The encrypted credential store

The client persists credentials through a CredentialStore. You get the right one automatically from the constructor arguments:

You passStore usedWhere the secret lives
credentials_file + encryption_keyFileCredentialStoreAES-GCM envelope on disk
encryption_key onlyMemoryCredentialStoreAES-GCM envelope in process memory
credential_storeyour implementationwherever you put it (e.g. a KMS-backed store)

Use a full-entropy 32-byte key. A 32-byte value, 64-char hex, or 32-byte base64 string is used directly; a plain passphrase is an explicit weaker fallback (scrypt-stretched) and must be at least 16 chars — anything shorter is rejected.

Next

  • Map your data — build canonical entities with the typed builders.
  • Sync — push the baseline snapshot, then deltas.