urmi-ai/CPaaS_WEBSOCKET_RELAY_ARCHITECTURE.md
2026-10-02 11:22:40 +05:30

973 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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": "<id>",
"call_id": "<id>",
"from": "<caller>",
"to": "<did>"
}
}
```
**Media (bidirectional)**
```json
{
"event": "media",
"stream_id": "<id>",
"media": {
"packet_id": 1,
"timestamp": 0,
"payload": "<base64 PCM>"
}
}
```
- 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": "<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 <provider_token>
```
```json
{
"autocallType": "5",
"destination": "<callee>",
"legACallerID": "<did>",
"eventID": "<unique_id>",
"voicebotProvider": "BONVOICE",
"voicebotURL": "wss://<your-relay-host>/v1/media/<session_token>"
}
```
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 <tenant_upstream_secret>` |
| **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://<relay>/v1/media/inbound` |
| `voicebotURL` in outbound API | `wss://<relay>/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 <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://<your-relay-or-ngrok-host>/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://<your-relay-or-ngrok-host>/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://<host>/api/v1/voice/ws/media` | Bonvoice outbound voicebotURL | IP allowlist + pending call match on `start` |
| `wss://<host>/api/v1/voice/media/{session_token}` | Legacy alias | Same handler |
Configure Bonvoice outbound API `voicebotURL`:
```
wss://<public-host>/api/v1/voice/ws/media
```
Set `VOICE_RELAY_PUBLIC_URL=wss://<public-host>/api/v1/voice` in production.
### 24.3 Console audio bridge (outbound testing)
| WebSocket | Purpose |
|-----------|---------|
| `wss://<host>/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.*