Skip to main content
Version: next

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 PyPI — pip install monoverse-voicebot does not work yet. Until it ships, install from source. The source is available on request from contact@edition.solutions.

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 Monoverse 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).
  • PATH — request.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.