Calls (WABA Voice)
Suporte a chamadas de voz via Meta WhatsApp Calling API. WABA-only.
Base path: /v1/calls
O gateway é stateless de áudio. Cliente final é responsável pelo WebRTC (browser ou app). O gateway só faz signaling com a Meta (offer/answer, accept/reject/hangup) e persiste metadata da call.
Arquitetura
┌──────────────┐ WebRTC P2P ┌──────────────┐
│ Browser │◄────────────►│ WhatsApp │
│ (seu cliente)│ │ do contato │
└──────┬───────┘ └──────────────┘
│ HTTPS REST (SDP)
▼
┌──────────────┐
│ wpp-gateway │ signaling ┌──────────────┐
│ /v1/calls │◄───────────►│ Meta WABA │
└──────────────┘ │ Cloud API │
└──────────────┘
Gateway responsabilidades:
- Persiste call lifecycle (
waba_calls). - Chama Meta para
make,accept,reject,hangup. - Repassa eventos da Meta via webhook (
call:statusfuturo — vermeta-webhook.ts). - Recebe upload de recording (mixagem local+remote feita no browser, MediaRecorder → octet-stream).
- Opcional: transcreve recording via Whisper.
Não faz: WebRTC, transcode, mixagem.
Lifecycle
INBOUND:
Webhook Meta → call:offer → seu app
│
▼
POST /v1/calls/:id/accept (SDP answer)
│
▼
ACCEPTED → IN_PROGRESS
│
▼
POST /v1/calls/:id/hangup
│
▼
COMPLETED
OUTBOUND:
POST /v1/calls (SDP offer)
│
▼
RINGING → ACCEPTED → IN_PROGRESS → COMPLETED
Status: RINGING, ACCEPTED, IN_PROGRESS, COMPLETED, REJECTED, MISSED, FAILED.
POST /v1/calls
Inicia uma call outbound. Cliente já gerou o SDP offer via WebRTC.
Body
| Campo | Tipo | Obrigatório | Limite |
|---|---|---|---|
phoneId | UUID | sim | Phone WABA |
to | string | sim | 5-60 chars (E.164 ou BSUID) |
sdpOffer | string | sim | ≥20 chars (SDP completo gerado por RTCPeerConnection.createOffer()) |
curl -X POST https://wpp.ogmma.com.br/v1/calls \
-H "X-API-Key: ak_live_..." \
-H "Content-Type: application/json" \
-d '{
"phoneId": "5f7b2e1c-...",
"to": "5511988887777",
"sdpOffer": "v=0\r\no=- 4611...\r\ns=-\r\nt=0 0\r\n..."
}'
Response 201
{
"id": "call-1234-...",
"accountId": "...",
"phoneId": "5f7b2e1c-...",
"metaCallId": "wamid.HBgL...",
"direction": "OUTBOUND",
"status": "RINGING",
"peer": "5511988887777",
"createdAt": "..."
}
Pré-requisito: o destinatário precisa ter dado call permission ao seu phone. Sem isso, Meta retorna call_permission_not_granted. Veja POST /v1/calls/permission-request abaixo.
GET /v1/calls
Lista calls.
Query
| Param | Default | Max |
|---|---|---|
phoneId | — | filtro |
status | — | filtro |
limit | 50 | 100 |
GET /v1/calls/:id
Detalhe. :id aceita UUID local ou metaCallId.
curl https://wpp.ogmma.com.br/v1/calls/call-1234-... \
-H "X-API-Key: ak_live_..."
Response 200 — objeto WabaCall.
Errors
| HTTP | Code | Quando |
|---|---|---|
| 404 | CALL_NOT_FOUND | — |
POST /v1/calls/:id/accept
Aceita uma call inbound. Cliente já gerou o SDP answer.
Body
| Campo | Tipo | Obrigatório | Limite |
|---|---|---|---|
sdpAnswer | string | sim | ≥20 chars |
curl -X POST https://wpp.ogmma.com.br/v1/calls/call-1234-.../accept \
-H "X-API-Key: ak_live_..." \
-d '{ "sdpAnswer": "v=0\r\no=- ..." }'
POST /v1/calls/:id/reject
Rejeita inbound (sem atender).
Body (opcional)
| Campo | Tipo |
|---|---|
reason | string ≤200 chars |
POST /v1/calls/:id/hangup
Encerra call ativa.
Body (opcional)
| Campo | Tipo |
|---|---|
reason | string ≤200 chars |
Call Permission Request
Pré-requisito Meta para business-initiated calls: o destinatário precisa ter aprovado calls do seu phone nos últimos 7 dias.
POST /v1/calls/permission-request
Envia uma mensagem solicitando permissão. Aparece como UI nativa Meta no app do destinatário.
Body
| Campo | Tipo |
|---|---|
phoneId | UUID |
to | string |
curl -X POST https://wpp.ogmma.com.br/v1/calls/permission-request \
-H "X-API-Key: ak_live_..." \
-d '{ "phoneId": "5f7b2e1c-...", "to": "5511988887777" }'
GET /v1/calls/permissions
Verifica se um contato já deu permissão.
Query: phoneId=<uuid>&to=<phone>
Recording
O gateway aceita upload do arquivo gravado no browser via MediaRecorder. Audio é armazenado em S3 (recordings/<accountId>/<callId>.<ext>). Lifecycle: 90 dias.
POST /v1/calls/:id/recording
Upload do binário cru. Body: Buffer (audio/webm, mp4, mp3, ogg). Limite: 50MB.
curl -X POST https://wpp.ogmma.com.br/v1/calls/call-1234-.../recording \
-H "X-API-Key: ak_live_..." \
-H "Content-Type: audio/webm" \
--data-binary @recording.webm
Response 201
{
"callId": "call-1234-...",
"s3Key": "recordings/<accountId>/call-1234-....webm",
"sizeBytes": 234567,
"contentType": "audio/webm"
}
Errors
| HTTP | Code | Quando |
|---|---|---|
| 400 | RECORDING_EMPTY | Body não é Buffer ou tamanho 0 |
| 404 | CALL_NOT_FOUND | — |
GET /v1/calls/:id/recording
Redirect para signed URL (TTL 10min).
Response 302 — Location: https://s3...
Errors
| HTTP | Code | Quando |
|---|---|---|
| 404 | RECORDING_NOT_FOUND | Recording ainda não foi upado |
Transcrição da gravação
Mesmo fluxo de Media → Transcribe.
POST /v1/calls/:id/transcribe
curl -X POST https://wpp.ogmma.com.br/v1/calls/call-1234-.../transcribe \
-H "X-API-Key: ak_live_..."
Response 202 (primeira chamada): { "status": "PENDING", "callId": "..." }.
Response 200 (cache hit): { "status": "COMPLETED", "text": "...", "language": "pt", "transcribedAt": "...", "cached": true }.
Errors
| HTTP | Code | Quando |
|---|---|---|
| 404 | RECORDING_NOT_FOUND | Sem recording uploaded ainda |
GET /v1/calls/:id/transcription
Estado atual.
{
"status": "COMPLETED",
"text": "Olá, gostaria de saber...",
"language": "pt",
"transcribedAt": "...",
"error": null
}
Errors
| HTTP | Code | Quando |
|---|---|---|
| 404 | TRANSCRIPTION_NOT_REQUESTED | Você não chamou POST /transcribe ainda |
Boas práticas
- Sempre solicite permission antes de tentar outbound — economiza falhas.
- Upload recording imediatamente no hangup — browsers podem perder o MediaRecorder ao trocar de tab.
- Mostre call status na UI via SSE (
channel:statusou eventos específicoscall:*) — mudanças são rápidas (RINGING ~5s timeout).