# CPaaS WebSocket Media Relay — Architecture & Integration Plan **Version:** 1.0 **Status:** Design **Scope:** Inbound + outbound voice media relay for Bonvoice (and future providers) **Primary goal:** Act as a **transparent WebSocket tunnel** between the telecom provider and the customer AI backend, with **authentication and routing control** at the relay layer and **no added media processing latency**. --- ## 1. Executive Summary Your CPaaS sits between: | Leg | Party | Protocol | |-----|-------|----------| | **Provider leg** | Bonvoice (or other CPaaS) | WebSocket + JSON media frames | | **Customer leg** | Tenant AI / voicebot backend | WebSocket + same JSON frame format | The relay **must not** decode, transcode, re-chunk, or buffer audio in the hot path. It forwards WebSocket text frames as-is after authentication and tenant routing. **You own:** - Provider credentials (Bonvoice token, DID inventory) - Customer API keys / JWT issuance - Session routing (`call_id`, `stream_id`, DID → tenant) - Audit logs, rate limits, billing metadata - Outbound call orchestration API **Customers own:** - AI logic, STT/TTS, conversation state - Their backend WebSocket endpoint (or pre-connect to your upstream socket) --- ## 2. Design Principles ### 2.1 Zero-processing media path ``` Bonvoice WS ──► Relay (auth + route) ──► Customer WS ▲ │ │ └────────────────────┴──────────────────────┘ raw JSON text frames, both directions ``` | Do | Don't | |----|-------| | Forward raw WebSocket text frames | `json.loads()` on every `media` event | | Parse only control events (`start`, `connected`, `end`) | Base64 decode / PCM transcoding | | Use async bidirectional copy (`asyncio.gather`) | Artificial `sleep()` or batching | | Colocate relay in same region as provider | Cross-region relay for real-time audio | | Pre-resolve tenant route before bridging | DB lookups inside media loop | **Target added latency:** < 5 ms p99 per hop (relay processing only, excluding network RTT to customer). ### 2.2 Fail closed on auth If authentication or routing fails, **reject before bridging**. Never forward media to an unauthenticated or unmapped session. ### 2.3 Provider URL is always yours Bonvoice (inbound template + outbound API) is configured with **your** `wss://` endpoint. Customers never expose their URL to the provider directly. This gives you full control over access, metering, and multi-tenancy. --- ## 3. System Context ```mermaid flowchart LR subgraph Provider["Telecom Provider (Bonvoice)"] DID[Virtual DID] PBX[PBX / Voicebot Platform] end subgraph Relay["Your CPaaS Relay"] GW[WebSocket Gateway] AUTH[Auth & Routing] API[REST Control API] SESS[Session Store] end subgraph Customer["Customer Tenant"] BOT[AI Voicebot Backend] end Caller((Caller)) --> DID DID --> PBX PBX <-->|wss JSON media| GW GW <--> AUTH AUTH <--> SESS API --> SESS Customer -->|API key| API GW <-->|wss JSON media| BOT ``` --- ## 4. Bonvoice Provider Protocol Reference Based on validated integration behaviour. ### 4.1 Connection - Transport: **WebSocket** (`wss://`), not HTTP POST - Bonvoice server IP observed: `103.131.25.5` (maintain allowlist) - Endpoint must use `wss://`, not `https://` ### 4.2 Message types **Call start** ```json { "event": "start", "data": { "stream_id": "", "call_id": "", "from": "", "to": "" } } ``` **Media (bidirectional)** ```json { "event": "media", "stream_id": "", "media": { "packet_id": 1, "timestamp": 0, "payload": "" } } ``` - Audio: PCM 16-bit, 8000 Hz, mono - Chunk size: **320 bytes** (20 ms), multiples of 320 only **Interruption (customer → provider)** ```json { "event": "clear", "stream_id": "" } ``` **Additional events observed** | Event | Direction | Relay action | |-------|-----------|--------------| | `connected` | Provider → relay | Forward as-is; optional metadata log | | `end` | Provider → relay | Forward, then close both legs gracefully | ### 4.3 Outbound API (control plane) ``` POST https://backend.pbx.bonvoice.com/autoDialManagement/autoCallBridging/ Authorization: Token ``` ```json { "autocallType": "5", "destination": "", "legACallerID": "", "eventID": "", "voicebotProvider": "BONVOICE", "voicebotURL": "wss:///v1/media/" } ``` Response: ```json { "responseCode": 200, "responseDescription": "Success", "responseType": "Success", "eventID": "..." } ``` --- ## 5. Relay Component Architecture ```mermaid flowchart TB subgraph ControlPlane["Control Plane"] REST[REST API] PROV[Provider Adapter - Bonvoice] TENANT[Tenant Registry] KEYS[API Key / JWT Service] end subgraph DataPlane["Data Plane - hot path"] PWS[Provider WebSocket Handler] TUNNEL[Frame Tunnel] CWS[Customer WebSocket Client/Handler] end REST --> PROV REST --> KEYS REST --> TENANT PWS --> TUNNEL TUNNEL --> CWS TENANT --> PWS KEYS --> PWS KEYS --> CWS ``` | Component | Responsibility | |-----------|----------------| | **Provider WebSocket Handler** | Accept Bonvoice WS; provider-side auth | | **Customer WebSocket Client** | Dial customer upstream WS (outbound leg) | | **Frame Tunnel** | Bidirectional raw frame copy | | **Session Store** | Redis: session_token → tenant, upstream URL, metadata | | **Tenant Registry** | DID → tenant; tenant → upstream WS URL + credentials | | **Provider Adapter** | Bonvoice outbound API, token rotation | | **REST API** | Customer-facing call control + config | --- ## 6. WebSocket Tunnel — Core Mechanism ### 6.1 Session lifecycle ``` 1. Provider connects → wss://relay/v1/media/{session_token} 2. Relay validates session_token (or resolves inbound by DID on `start`) 3. Relay opens upstream → wss://customer/configured-endpoint 4. Relay starts two async copy loops 5. On `end` or disconnect → cancel both loops, purge session ``` ### 6.2 Tunnel pseudocode (hot path) ```python async def tunnel(provider_ws, customer_ws): async def p2c(): async for raw in provider_ws: await customer_ws.send(raw) # no parse, no modify async def c2p(): async def c2p(): async for raw in customer_ws: await provider_ws.send(raw) await asyncio.gather(p2c(), c2p()) ``` ### 6.3 Optional control-plane hook (non-blocking) Parse **only the first** `start` frame (and `end`) in a side task for logging/billing. Never block the copy loop. ```python async def observe_control_events(raw: str, session: Session): if '"event":"start"' in raw: # or parse once with orjson session.mark_started() elif '"event":"end"' in raw: session.mark_ended() ``` Use string prefix check or single parse on non-media events only. ### 6.4 Upstream connection modes | Mode | Latency | Use case | |------|---------|----------| | **A. Relay dials customer on `start`** | +1 WS handshake (~30–150 ms) | Simplest; acceptable for most bots | | **B. Customer pre-connects to relay** | Lowest | Customer opens `wss://relay/v1/upstream/{session_token}` before answering | | **C. Persistent tenant socket** | Low | Customer keeps one WS open; relay multiplexes by `stream_id` (complex) | **Recommendation:** Start with **Mode A**; offer **Mode B** for latency-sensitive tenants. --- ## 7. Authentication Model Authentication is enforced at **three layers**. ### 7.1 Layer 1 — Provider leg (Bonvoice → Relay) | Method | Implementation | |--------|----------------| | **IP allowlist** | Only accept WS from known Bonvoice egress IPs | | **Session token in URL** | Outbound: `wss://relay/v1/media/{session_token}` — token is single-use, TTL 5 min | | **Inbound DID routing** | On `start`, map `to` (DID) → tenant; reject unknown DID | | **mTLS (optional)** | Mutual TLS if Bonvoice supports client certs | Provider token (`Authorization: Token ...`) is used **only** on outbound REST API calls **from relay to Bonvoice**, never exposed to customers. ### 7.2 Layer 2 — Relay control plane (Customer → Relay REST) | Method | Implementation | |--------|----------------| | **API key** | `Authorization: Bearer sk_live_...` on all REST endpoints | | **Scoped permissions** | `calls:write`, `numbers:read`, etc. | | **Rate limiting** | Per-tenant token bucket | ### 7.3 Layer 3 — Customer leg (Relay → Customer backend) Choose one per tenant (configured in dashboard): | Method | Header / mechanism | |--------|-------------------| | **Static secret** | `Authorization: Bearer ` | | **Signed JWT** | Short-lived JWT signed by relay, claims: `tenant_id`, `session_id`, `call_id` | | **HMAC query param** | `wss://customer/bot?sig=HMAC(session_token, secret)` | | **mTLS** | Client cert per tenant | Customer validates the relay; relay validates the customer using pre-shared secret or JWT. ### 7.4 Session token design ``` session_token = base64url(tenant_id || call_uuid || HMAC-SHA256(...)) ``` Properties: - Single-use or idempotent per `eventID` - TTL: 300 seconds - Bound to: tenant, direction, destination/caller, DID - Stored in Redis before outbound API call --- ## 8. Inbound Call Flow ```mermaid sequenceDiagram participant Caller participant Bonvoice participant Relay participant Redis participant Customer Caller->>Bonvoice: Inbound call to DID Bonvoice->>Relay: wss connect /v1/media/inbound Bonvoice->>Relay: event=start {to: DID, from, call_id, stream_id} Relay->>Redis: Lookup DID → tenant + upstream URL alt Unknown DID or tenant suspended Relay-->>Bonvoice: Close WS (4403) else OK Relay->>Customer: wss connect + auth headers Relay->>Relay: Start bidirectional tunnel loop Every 20ms Bonvoice->>Relay: event=media Relay->>Customer: forward raw frame Customer->>Relay: event=media Relay->>Bonvoice: forward raw frame end Bonvoice->>Relay: event=end Relay->>Customer: forward end Relay->>Relay: Close both sockets end ``` ### 8.1 Bonvoice configuration (per DID) | Field | Value | |-------|-------| | Provider | BONVOICE | | Endpoint | `wss://media.yourcpaas.com/v1/media/inbound` | All tenants share one inbound WS URL; routing happens on `start.data.to` (DID). ### 8.2 Inbound routing table ``` DID 7946350706 → tenant_abc → wss://bot.customer.com/voice DID 7946350707 → tenant_xyz → wss://api.other.com/ws/bonvoice ``` --- ## 9. Outbound Call Flow ```mermaid sequenceDiagram participant Customer participant Relay participant Redis participant Bonvoice participant Callee Customer->>Relay: POST /v1/calls {to, from_did} + API key Relay->>Relay: Validate API key, quota Relay->>Redis: Store session_token → tenant, upstream, metadata Relay->>Bonvoice: POST autoCallBridging {voicebotURL: wss://relay/.../session_token} Bonvoice-->>Relay: 200 Success Relay-->>Customer: {call_id, session_id, status: initiated} opt Mode B - pre-connect Customer->>Relay: wss /v1/upstream/{session_token} end Bonvoice->>Callee: Ring Callee->>Bonvoice: Answer Bonvoice->>Relay: wss /v1/media/{session_token} Relay->>Redis: Validate session_token Relay->>Customer: wss connect upstream (Mode A) or attach pre-connect (Mode B) Relay->>Relay: Tunnel media both ways ``` ### 9.1 Customer REST API (your CPaaS) ``` POST /v1/calls Authorization: Bearer sk_live_... Content-Type: application/json { "to": "9149154337", "from": "7946350706", "upstream": "wss://bot.customer.com/voice", "metadata": { "campaign_id": "xyz" } } ``` Response: ```json { "id": "call_01J...", "session_token": "...", "status": "initiated", "provider_event_id": "11A31A12FF1C4455" } ``` Relay internally calls Bonvoice with `voicebotURL = wss://media.yourcpaas.com/v1/media/{session_token}`. --- ## 10. URL & Routing Scheme | Endpoint | Purpose | Auth | |----------|---------|------| | `wss://media.../v1/media/inbound` | All inbound Bonvoice streams | IP allowlist + DID routing on `start` | | `wss://media.../v1/media/{session_token}` | Outbound + optional direct sessions | Session token validation | | `wss://media.../v1/upstream/{session_token}` | Customer pre-connect (Mode B) | API key or session_token | | `POST /v1/calls` | Initiate outbound | Customer API key | | `GET /v1/calls/{id}` | Call status | Customer API key | | `POST /v1/numbers` | Assign DID to tenant | Customer API key | --- ## 11. Multi-Tenancy & Data Model ### 11.1 Core entities ``` Tenant - id, name, status - api_key_hash - upstream_ws_url - upstream_auth_type, upstream_secret - quota_limits PhoneNumber (DID) - e164, tenant_id, provider, provider_account_id CallSession - session_token, tenant_id, direction - call_id, stream_id, from, to - status: pending | bridging | active | ended | failed - created_at, answered_at, ended_at ProviderAccount - provider: bonvoice - token_encrypted, default_caller_id ``` ### 11.2 Redis session record (ephemeral) ```json { "session_token": "abc123", "tenant_id": "ten_01", "direction": "outbound", "upstream_ws": "wss://bot.customer.com/voice", "upstream_auth": "Bearer xxx", "to": "9149154337", "from": "7946350706", "status": "pending", "expires_at": 1699999999 } ``` TTL matches Bonvoice ring timeout + margin (e.g. 10 minutes). --- ## 12. Latency Budget | Stage | Target | |-------|--------| | Relay frame forward (provider → customer) | < 2 ms p99 | | Relay frame forward (customer → provider) | < 2 ms p99 | | Session lookup (Redis) | < 1 ms | | Upstream WS connect (Mode A, one-time) | 30–150 ms (not per frame) | | JSON parse on hot path | **0** (avoided) | | Audio transcoding | **0** (avoided) | **20 ms audio frames:** relay must sustain > 50 frames/sec per call with headroom for 500+ concurrent calls per node. --- ## 13. Failure Handling | Scenario | Behaviour | |----------|-----------| | Invalid session token | Close provider WS immediately with code `4401` | | Unknown inbound DID | Close after `start` with `4403`; log alert | | Customer upstream unreachable | Retry connect 2× with 100 ms backoff; then close provider leg | | Customer WS drops mid-call | Close provider leg; emit webhook `call.upstream_disconnected` | | Provider WS drops | Close customer leg | | Bonvoice sends POST not WS | Return `426` + log; misconfigured URL | | Provider API failure on outbound | Return error to customer; do not create session | ### 13.1 Webhook events (control plane, async) Deliver to customer via HTTP POST (not in media path): - `call.initiated` - `call.answered` - `call.media_started` - `call.ended` - `call.failed` --- ## 14. Security Checklist - [ ] Provider token stored encrypted (KMS / vault); never in logs - [ ] Customer API keys stored as hashes - [ ] Session tokens: cryptographically random, single-use, short TTL - [ ] TLS 1.2+ on all WS and REST - [ ] IP allowlist for Bonvoice egress - [ ] Per-tenant rate limits on `/v1/calls` - [ ] No customer can specify arbitrary `voicebotURL` to Bonvoice — relay always injects its own URL - [ ] Audit log: who initiated call, DID used, duration, bytes relayed --- ## 15. Observability ### 15.1 Metrics (per call) - `relay_session_active{tenant, direction}` - `relay_frame_forward_total{direction=provider_to_customer|customer_to_provider}` - `relay_frame_forward_latency_ms` (histogram) - `relay_upstream_connect_duration_ms` - `relay_auth_failure_total{reason}` ### 15.2 Tracing - Trace ID = `call_id` or `session_token` - Span: `provider.accept` → `session.resolve` → `upstream.connect` → `tunnel.active` ### 15.3 Logging Log control events only (`start`, `end`, `connected`). Do **not** log media payloads. --- ## 16. Implementation Roadmap ### Phase 1 — MVP tunnel (2–3 weeks) - [ ] WebSocket gateway with raw frame tunnel - [ ] Redis session store - [ ] Outbound: `POST /v1/calls` → Bonvoice API → session routing - [ ] Inbound: DID → tenant lookup on `start` - [ ] API key auth on REST - [ ] Provider IP allowlist - [ ] Basic metrics + structured logs ### Phase 2 — Production hardening (2 weeks) - [ ] Customer upstream auth (JWT / HMAC) - [ ] Mode B pre-connect upstream endpoint - [ ] Webhooks for call lifecycle - [ ] Graceful shutdown / connection draining - [ ] Horizontal scaling behind L4 LB with sticky sessions OR shared Redis session state ### Phase 3 — Platform (ongoing) - [ ] Multi-provider adapter interface (abstract Bonvoice) - [ ] Dashboard: DID management, API keys, call logs - [ ] Billing meter: call duration + minute rounding - [ ] HA Redis cluster, multi-region consideration --- ## 17. Deployment Topology ```mermaid flowchart TB subgraph Internet BV[Bonvoice] CUST[Customer Backends] end subgraph YourCloud["Your CPaaS Region (e.g. ap-south-1)"] LB[L4/L7 Load Balancer] R1[Relay Node 1] R2[Relay Node 2] REDIS[(Redis Cluster)] API[Control API] DB[(PostgreSQL)] end BV -->|wss| LB LB --> R1 LB --> R2 R1 --> REDIS R2 --> REDIS API --> DB API --> REDIS R1 -->|wss| CUST R2 -->|wss| CUST CUST -->|REST| API ``` **Notes:** - WebSocket LB must support connection upgrade and long-lived connections - Sticky sessions optional if session state is fully in Redis - Run relay nodes close to Bonvoice infrastructure (India) to minimize RTT --- ## 18. Provider Abstraction (future-proof) Define a provider interface so Bonvoice is one adapter: ```python class VoiceProvider(Protocol): async def initiate_outbound(self, req: OutboundCallRequest) -> ProviderCallRef: ... def media_websocket_path(self) -> str: ... def validate_inbound(self, start_event: dict) -> bool: ... ``` Bonvoice adapter implements `autoCallBridging` and protocol quirks (`connected`, `end` events). --- ## 19. Customer Integration Guide (summary for docs) ### 19.1 Inbound 1. Customer claims a DID via your dashboard/API 2. Customer registers upstream WebSocket URL 3. You configure Bonvoice template → `wss://media.yourcpaas.com/v1/media/inbound` 4. Customer implements Bonvoice JSON protocol on their WS server 5. Customer receives `start`, then `media` frames; responds with `media` and optional `clear` ### 19.2 Outbound 1. Customer calls `POST /v1/calls` with API key 2. Optional: pre-connect to `wss://media.yourcpaas.com/v1/upstream/{session_token}` 3. Relay bridges Bonvoice ↔ customer when callee answers 4. Customer handles protocol identically to direct Bonvoice integration ### 19.3 What customers do NOT need - Bonvoice account or token - Direct exposure of their WS URL to Bonvoice - Provider-specific outbound API knowledge --- ## 20. Risks & Mitigations | Risk | Mitigation | |------|------------| | Added latency from relay hop | Raw tunnel, no parsing, regional colocation | | Upstream connect delay (Mode A) | Offer pre-connect Mode B | | Single relay node failure | Multiple nodes + LB + Redis | | Token leakage in URL | Short TTL, single-use, HTTPS/WSS only | | Tenant sends malformed media | Provider enforces 320-byte rule; relay forwards as-is | | Bonvoice protocol changes | Provider adapter layer + contract tests | --- ## 21. Success Criteria | Metric | Target | |--------|--------| | Relay processing latency | < 5 ms p99 per frame | | Call setup (outbound, excl. ring time) | < 200 ms from answer to first media frame forwarded | | Auth failure rate (invalid sessions) | 100% blocked, 0% bridged | | Concurrent calls per node | ≥ 500 with < 60% CPU | | Uptime | 99.9% for media gateway | --- ## 22. Appendix — Mapping from Current PoC Your existing PoC maps to the relay as follows: | PoC file | Relay equivalent | |----------|------------------| | `main.py` (`handle_media_stream`) | Replace playback logic with `tunnel(provider_ws, customer_ws)` | | `outbound_call.py` | Becomes internal `BonvoiceProvider.initiate_outbound()` | | `audio_utils.py` | **Not used in relay** — only in test/demo tooling | | Bonvoice template endpoint | `wss:///v1/media/inbound` | | `voicebotURL` in outbound API | `wss:///v1/media/{session_token}` | --- ## 23. Appendix — Bonvoice Credentials & API Reference (PoC / Test) > **Security warning:** This section contains live credentials used during PoC testing. > Do **not** commit this file to a public repository. Rotate the provider token before production. > Store secrets in a vault (e.g. AWS Secrets Manager, HashiCorp Vault) in the relay implementation. ### 23.1 Provider account credentials | Item | Value | Usage | |------|-------|-------| | **Auth Token** | `8de97c5932280c84c4bbf36140a1ec7ab7ae0b23` | `Authorization: Token ` on Bonvoice REST APIs | | **Virtual DID** | `7946350706` | Inbound number; use as `legACallerID` on outbound | | **Provider name** | `BONVOICE` | Voicebot template + `voicebotProvider` field | ### 23.2 Bonvoice API endpoints | API | Method | URL | |-----|--------|-----| | **Auto Call Bridging (outbound voicebot)** | `POST` | `https://backend.pbx.bonvoice.com/autoDialManagement/autoCallBridging/` | | **Media stream (WebSocket)** | `GET` (Upgrade) | Configured per template / `voicebotURL` — must be `wss://`, not `https://` | **Outbound request headers:** ```http Authorization: Token 8de97c5932280c84c4bbf36140a1ec7ab7ae0b23 Content-Type: application/json ``` **Outbound request body (validated PoC example):** ```json { "autocallType": "5", "destination": "9149154337", "legACallerID": "7946350706", "eventID": "11A31A12FF1C4455", "voicebotProvider": "BONVOICE", "voicebotURL": "wss:///ws/media" } ``` **Successful outbound response (observed):** ```json { "responseCode": 200, "responseDescription": "Success", "responseType": "Success", "eventID": "11A31A12FF1C4455" } ``` | Field | Description | |-------|-------------| | `autocallType` | `"5"` — voicebot auto-call type per Bonvoice API | | `destination` | Callee E.164 (no `+` in PoC tests) | | `legACallerID` | Caller ID shown to callee; use assigned DID | | `eventID` | Unique tracking ID (UUID / random hex recommended) | | `voicebotProvider` | Must be `BONVOICE` for Bonvoice voicebot templates | | `voicebotURL` | **Your relay WebSocket URL** — Bonvoice connects here when call is answered | ### 23.3 Inbound voicebot template (Bonvoice portal) Configure under **Voicebot Templates → Create new template**: | Field | PoC value | |-------|-----------| | Template Name | `test` (or any label) | | Provider | `BONVOICE` | | Endpoint | `wss:///ws/media` | **Important:** Use `wss://` (WebSocket Secure). Using `https://` causes Bonvoice to send HTTP `POST` instead of a WebSocket upgrade, resulting in `404 Not Found`. ### 23.4 Network & infrastructure (observed during PoC) | Item | Value | Notes | |------|-------|-------| | **Bonvoice media server IP** | `103.131.25.5` | Source IP for inbound WebSocket connections — add to provider allowlist | | **Local dev server** | `http://0.0.0.0:8000` | Uvicorn PoC | | **PoC WebSocket path** | `/ws/media` | Also accepts `/` in PoC | | **ngrok URL (ephemeral)** | `https://1af5-2401-4900-88ce-916c-2c02-6571-5bcd-b1eb.ngrok-free.app` | Changes every ngrok restart; WSS = same host with `wss://` | **PoC WebSocket endpoint used in tests:** ``` wss://1af5-2401-4900-88ce-916c-2c02-6571-5bcd-b1eb.ngrok-free.app/ws/media ``` ### 23.5 Test phone numbers | Number | Role | |--------|------| | `7946350706` | Assigned virtual DID (inbound destination / outbound caller ID) | | `9149154337` | Test mobile used for outbound dial + inbound caller during PoC | | `9846098460` | Example destination from Bonvoice API documentation | ### 23.6 Observed call / stream identifiers (PoC logs) Successful inbound WebSocket session (for correlation testing): | Field | Value | |-------|-------| | `stream_id` | `6379` | | `call_id` | `12775` | | `from` | `9149154337` | | `to` | `7946350706` | Additional WebSocket events observed from Bonvoice (forward in relay; do not drop): | Event | When | |-------|------| | `connected` | Immediately after WS handshake, before `start` | | `start` | Call metadata — use for DID routing / session binding | | `media` | Audio frames (320-byte PCM chunks, Base64) | | `clear` | Barge-in / flush playback buffer | | `end` | Call teardown | ### 23.7 Ready-to-run curl (outbound) Replace `voicebotURL` with your current relay or ngrok WSS URL: ```bash curl -X POST "https://backend.pbx.bonvoice.com/autoDialManagement/autoCallBridging/" \ -H "Authorization: Token 8de97c5932280c84c4bbf36140a1ec7ab7ae0b23" \ -H "Content-Type: application/json" \ -d '{ "autocallType": "5", "destination": "9149154337", "legACallerID": "7946350706", "eventID": "TEST'"$(date +%s)"'", "voicebotProvider": "BONVOICE", "voicebotURL": "wss://YOUR-RELAY-HOST/ws/media" }' ``` PoC script equivalent (auto-detects ngrok URL): ```bash cd test_bonvoice source .venv/bin/activate python3 outbound_call.py 9149154337 ``` ### 23.8 Environment variables (recommended for relay implementation) | Variable | PoC / example value | |----------|---------------------| | `BONVOICE_TOKEN` | `8de97c5932280c84c4bbf36140a1ec7ab7ae0b23` | | `BONVOICE_DID` | `7946350706` | | `BONVOICE_VOICEBOT_URL` | `wss://media.yourcpaas.com/v1/media/inbound` | | `BONVOICE_API_URL` | `https://backend.pbx.bonvoice.com/autoDialManagement/autoCallBridging/` | | `BONVOICE_MEDIA_ALLOWLIST` | `103.131.25.5` | ### 23.9 Audio format requirements (provider-enforced) | Parameter | Value | |-----------|-------| | Encoding | PCM | | Bit depth | 16-bit | | Sample rate | 8000 Hz | | Channels | Mono | | Chunk size | **320 bytes** (multiples of 320 only) | | Wire format | Base64 in JSON `media.payload` | --- ## 24. Urmi CPaaS — Implemented Customer API Routes Urmi exposes production voice relay endpoints under `/api/v1/voice` (REST + WebSocket). All outbound calls validate that `from` is a **purchased DID** assigned to the authenticated account. ### 24.1 REST control plane | Method | Path | Auth | Description | |--------|------|------|-------------| | `POST` | `/api/v1/voice/calls` | API key or session cookie | Initiate outbound call | | `GET` | `/api/v1/voice/calls/{id}` | API key or session cookie | Poll call status | | `GET` | `/api/v1/voice/docs` | API key or session cookie | API reference for integrators | **Create outbound call** ```http POST /api/v1/voice/calls Authorization: Bearer urmi_live_... Content-Type: application/json { "to": "9149154337", "from": "7946350706", "bridge_mode": "upstream", "upstream": "wss://bot.customer.com/voice" } ``` Console testing (Urmi dashboard at `/console/voice`): ```json { "to": "9149154337", "from": "7946350706", "bridge_mode": "console" } ``` Validation rules: - `from` must match a `phone_numbers` row with `status=assigned` and `user_id` = caller - `upstream` is required when `bridge_mode=upstream` - Relay always injects Bonvoice `voicebotURL` — customers never pass provider URLs ### 24.2 Media relay (provider leg) | WebSocket | Purpose | Auth | |-----------|---------|------| | `wss:///api/v1/voice/ws/media` | Bonvoice outbound voicebotURL | IP allowlist + pending call match on `start` | | `wss:///api/v1/voice/media/{session_token}` | Legacy alias | Same handler | Configure Bonvoice outbound API `voicebotURL`: ``` wss:///api/v1/voice/ws/media ``` Set `VOICE_RELAY_PUBLIC_URL=wss:///api/v1/voice` in production. ### 24.3 Console audio bridge (outbound testing) | WebSocket | Purpose | |-----------|---------| | `wss:///api/v1/voice/console` | Browser mic/speaker bridge for `bridge_mode: console` | When placing a console-bridged outbound call from `/console/voice`, the account must have an active console WebSocket. Provider media is forwarded to the browser; browser mic frames are relayed back to Bonvoice. ### 24.4 Environment variables (Urmi) | Variable | Example | |----------|---------| | `REDIS_URL` | `redis://redis:6379/0` | | `BONVOICE_TOKEN` | `8de97c5932280c84c4bbf36140a1ec7ab7ae0b23` | | `BONVOICE_API_URL` | `https://backend.pbx.bonvoice.com/autoDialManagement/autoCallBridging/` | | `BONVOICE_MEDIA_ALLOWLIST` | `103.131.25.5` | | `VOICE_RELAY_PUBLIC_URL` | `wss://media.yourcpaas.com/api/v1/voice` | --- ## 25. Next Steps 1. **Approve** URL scheme and auth model (Section 7, 10) 2. ~~**Implement** Phase 1 tunnel with Redis session store~~ (Urmi MVP shipped) 3. **Migrate** Bonvoice template + outbound API to relay URLs 4. **Load test** at 320-byte / 20 ms frame rate per call 5. **Document** customer-facing API and WebSocket protocol (derived from this plan) --- *Document maintained by CPaaS platform team. Update when adding providers or changing auth model.*