first commit

This commit is contained in:
debu-2 2026-10-02 11:22:40 +05:30
commit efa5080897
127 changed files with 18610 additions and 0 deletions

View file

@ -0,0 +1,973 @@
# 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.*