# WhatsApp Web call integration (Spec 004 + Spec 005) This document describes the call-control surface added by Spec 004 (call control, catalog/orders, `/chat/reply`, webhook v1). Call media is carried by the native Go calls engine (§3). The Spec 005 call-media sidecar bridge was **removed** (commit `fc3c35c`); §3.H is kept only as a historical record. ## Design constraints honored - **No new environment variable** for call control, except the opt-in `GLOBAL_CALL_INITIATE_DIAL_ALIAS` for `/call/initiate`. - **No new database table.** No migration was added; `migrations.go` is unchanged. Call state is **not** persisted. - **Webhook contract intact.** Enrichment is nested under a single additive `v1` key (see `docs/webhook-events.md`). - **Honest failure modes.** Endpoints return `501`/`503` truthfully instead of pretending a capability exists. ## 1. Call control (Spec 004) Always available — no feature flag. | Endpoint | Method | Behavior | |-----------------------|--------|---------------------------------------------------------------------| | `/call/reject/send` | POST | Existing endpoint, unchanged. | | `/call/reject` | POST | Reject + optional follow-up text. No persistence. | | `/call/accept` | POST | Send accept stanza. | | `/call/preaccept` | POST | Send preaccept (ringing) stanza. | | `/call/terminate` | POST | Send terminate stanza (default reason `hangup`). | | `/call/initiate` | POST | **501 Not Implemented** (legacy route; `reason` keeps the original text). Place outbound calls with `POST /call/dial` (native calls engine), or set `GLOBAL_CALL_INITIATE_DIAL_ALIAS=true` to make this route an alias of it. | There is **no** `/call/history` and **no** `/call/missed` — those required a `call_log` table, which is intentionally not added. ## 2. Catalog / products / orders (Spec 004) `/business/order/{orderID}`, `/business/catalog/{businessJID}`, `/business/products/{businessJID}`, `/business/collections/{businessJID}`, `/business/catalog/send`, `/business/product/send`, `/business/product-list/send`, `/business/shop/send`. - Catalog reads are cached in-memory for 300s (a constant, not an env var). - Catalog/product/collection reads return **503 `catalog_unavailable`** until the upstream read stanza is validated (honest stub). - `/business/shop/send` is gated by the project's **existing** enterprise license mechanism (`ensureEnterpriseLicense`, the same gate as carousel/flows/interactive) and returns **403** when not licensed. No new env var, no new license key. ## 3. Call media — native calls engine Call media runs in-process on `voip.Manager` (`voip/manager.go`), attached per user in `call_engine.go` when `calls_enabled` is true and `call_inbound_mode` is not `reject`. Configuration lives in `users` columns and is read/written with `GET/PUT /call/config` (applied on the next reconnect). | Endpoint | Method | Behavior | |--------------------------------------------|--------|---------------------------------------------------------------| | `/call/status` | GET | List live calls. | | `/call/dial` | POST | Place an outbound call (`phone`, optional `video`). | | `/call/answer` / `/call/hangup` | POST | Answer / hang up a call by `callId`. | | `/call/play` | POST | Play audio (`audioUrl` or `audioBase64`; WAV/MP3/Ogg-Opus). | | `/call/record/start` / `/call/record/stop` | POST | Start / stop recording the peer audio. | | `/call/{call_id}/stream` | GET | WebSocket, bidirectional **s16le 16 kHz mono PCM**, 60 ms frames (960 samples / 1920 bytes). Mutually exclusive with recording on the same call. | | `/call/{call_id}/video/{stream,state,stats}` | GET | Video (H.264 Annex-B), see `call-video-guide.md`. | Inbound modes (`call_inbound_mode`): `manual` (the app answers and drives the call through the API), `webhook` (default; the engine answers and emits lifecycle webhooks, recording when `call_record` is on), `reject` (engine off; legacy reject path). `bot`, `ivr` and `ai` are accepted by `/call/config` but currently **reject** the inbound call (voice agent unavailable in this build). See [`call-audio-implementation-guide.md`](./call-audio-implementation-guide.md) for the browser PCM client. ### 3.H Historical — Spec 005 sidecar bridge (REMOVED) > **Removed in `fc3c35c`.** None of the routes below exist anymore > (`/session/sidecar/config`, `/call/media/*` return 404) and `sidecar_client.go` > was deleted. The text is kept only as a record of the old design. ZuckZapGo only **proxies signaling** to an external WebRTC sidecar. The sidecar is a separate process/repo; this build never embeds media. #### Architecture (removed) ``` Operator browser ── HTTPS ──> ZuckZapGo /call/media/* ──> per-user sidecar (HTTP, X-Sidecar-Token) │ └── webhook v1.call.* enriches the call lifecycle ``` #### Per-user sidecar configuration (removed) The project persists per-user transport config (RabbitMQ, S3) as `users` table columns via migrations. Adding sidecar columns would require a migration, which the zero-schema constraint forbids. Therefore the sidecar config is **per-user, held in memory**, configured at runtime: | Endpoint | Method | Behavior | |---------------------------|--------|-------------------------------------------------------| | `/session/sidecar/config` | POST | Set `{enabled, url, token}` for the calling user. | | `/session/sidecar/config` | GET | Returns `{enabled, url, has_token}` (token masked). | | `/session/sidecar/config` | DELETE | Clears the user's sidecar config. | ```bash curl -X POST $BASE/session/sidecar/config \ -H "token: $USER_TOKEN" -H "Content-Type: application/json" \ -d '{"enabled":true,"url":"http://127.0.0.1:7700","token":"shared-secret"}' ``` Configuration is in-memory: it is per-user and is re-applied by the operator after a restart (no env var, no schema migration). This is the deliberate, zero-schema design. #### Media endpoints (removed) | Endpoint | Method | Behavior | |--------------------------------------------|--------|---------------------------------------------------| | `/call/media/{call_id}/operator-sdp` | POST | Proxy operator SDP offer/answer to the sidecar. | | `/call/media/{call_id}/operator-ice` | POST | Proxy an operator ICE candidate. | | `/call/media/{call_id}/operator-attach` | POST | Signal the sidecar to attach the operator PC. | | `/call/media/{call_id}/hangup` | POST | Tear down the media session (returns `duration_ms`). | #### 503 semantics (removed) - **No sidecar configured for the user** → `503 sidecar_not_configured` with a hint to call `POST /session/sidecar/config`. - **Sidecar configured but unreachable / 5xx** → `503 sidecar_unreachable` (with `retry_after_seconds` on operator-sdp). The media endpoints never fake success. Until an operator configures a reachable sidecar, every media call answers 503 truthfully. ## 4. curl examples ```bash # Reject an incoming call with a follow-up message curl -X POST $BASE/call/reject -H "token: $USER_TOKEN" \ -d '{"call_id":"","call_from":"","reject_type":"declined","message":"Retorno em breve"}' # Place an outbound call with the native engine, then hang up curl -X POST $BASE/call/dial -H "token: $USER_TOKEN" -d '{"phone":""}' curl -X POST $BASE/call/hangup -H "token: $USER_TOKEN" -d '{"callId":""}' ``` ## 5. See also - `docs/webhook-events.md` — the `v1` webhook enrichment schema. - `static/api/spec.yml` — OpenAPI definitions for every endpoint above.