Llamadas
El motor ve las llamadas que entran al número: cada señal (la oferta, el corte, la aceptación
en otro dispositivo) llega como un evento call.*, y la llamada queda registrada con su estado. Tu
app puede rechazar una llamada que está sonando, o la instancia puede rechazarlas sola y
mandarle un texto a quien llamó.
Requisitos
Sección titulada «Requisitos»| Operación | Scope |
|---|---|
GET /v1/instances/{id}/calls (las llamadas y su estado) |
calls:manage |
POST /v1/instances/{id}/calls/{call_id}/reject (rechazar) |
calls:manage |
POST …/calls/{call_id}/accept y …/offer (siempre 501) |
calls:manage |
PATCH /v1/instances/{id} con config.rechazar_llamadas y config.mensaje_rechazo |
instances:manage |
GET /v1/instances/{id}/events (los eventos call.*) |
events:read |
Recibir las llamadas
Sección titulada «Recibir las llamadas»Cada señal de una llamada entrante es un evento del ledger, deduplicado por llamada y por tipo:
| Evento | Qué es |
|---|---|
call.offer |
Una llamada entrante (la oferta). Trae video y elementos. |
call.offer_notice |
El aviso de una oferta. |
call.preaccept |
Una señal previa a la aceptación. |
call.accept |
Se atendió en otro dispositivo del número (el teléfono). |
call.terminate |
Terminó: colgaron, nadie atendió o se rechazó en otro lado. motivo es el código que informa WhatsApp. |
call.reject |
WhatsApp informó un rechazo. |
call.signal |
Cualquier otra señal de la llamada. |
call.rejected |
El motor la rechazó: rechazo dice quién lo pidió (app, operador o automatico). |
{ "call_id": "CALL_ID", "de": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": ["CONTACTO@s.whatsapp.net"] }, "video": false, "momento": "2026-10-24T15:40:00Z", "elementos": ["audio", "net"]}dees quien llama, como un peer.videoestruesi la oferta trae un elementovideo(una videollamada).elementosson solo los nombres de las partes de la oferta, para diagnóstico.- En una llamada de grupo, viene además
grupo, el peer del grupo.
{ "call_id": "CALL_ID", "de": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": ["CONTACTO@s.whatsapp.net"] }, "video": false, "motivo": "timeout", "momento": "2026-10-24T15:42:00Z"}Consultar las llamadas
Sección titulada «Consultar las llamadas»GET /v1/instances/{id}/calls lista las llamadas, las más nuevas primero, con su estado:
entrante (sonando, o sin una señal de fin), aceptada (en otro dispositivo), terminada o
rechazada. Con entrantes=true, solo las que siguen sonando: entrante y llegadas hace menos de
2 minutos.
curl "https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/calls?entrantes=true" \ -H "Authorization: Bearer TU_API_KEY"// El SDK de Go no trae las llamadas: es HTTP directo.req, _ := http.NewRequestWithContext(ctx, http.MethodGet, c.Base+"/v1/instances/"+instanciaID+"/calls?entrantes=true", nil)req.Header.Set("Authorization", "Bearer "+c.Token)res, err := c.HTTP.Do(req)if err != nil { return err}defer res.Body.Close()var out struct { Llamadas []struct { CallID string `json:"call_id"` De motor.Peer `json:"de"` Video bool `json:"video"` Estado string `json:"estado"` } `json:"llamadas"`}err = json.NewDecoder(res.Body).Decode(&out)const res = await fetch( `https://motor.ejemplo.com/v1/instances/${instanciaId}/calls?entrantes=true`, { headers: { Authorization: `Bearer ${apiKey}` } },);const { llamadas } = await res.json();{ "llamadas": [ { "call_id": "CALL_ID", "de": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": ["CONTACTO@s.whatsapp.net"] }, "video": false, "estado": "entrante", "recibida_en": "2026-10-24T15:40:00Z", "actualizada_en": "2026-10-24T15:40:00Z" } ]}Rechazar una llamada
Sección titulada «Rechazar una llamada»POST /v1/instances/{id}/calls/{call_id}/reject acepta un comando durable call.reject y responde
202. No lleva cuerpo ni Idempotency-Key: es idempotente por llamada (rechazar dos veces la
misma devuelve el mismo comando). Una llamada que la instancia no conoce es 404.
El rechazo va al dispositivo y al creador exactos con los que llegó la oferta. Cuando sale, el
comando queda completado y llega call.rejected con rechazo: app. completado quiere decir que
salió: WhatsApp no confirma el rechazo, y el corte en el teléfono de quien llama puede llegar
después como call.terminate.
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/calls/CALL_ID/reject \ -H "Authorization: Bearer TU_API_KEY"// El SDK de Go no trae el rechazo: es HTTP directo. Después, el comando con c.Comando.req, _ := http.NewRequestWithContext(ctx, http.MethodPost, c.Base+"/v1/instances/"+instanciaID+"/calls/"+url.PathEscape(callID)+"/reject", nil)req.Header.Set("Authorization", "Bearer "+c.Token)res, err := c.HTTP.Do(req) // 202if err != nil { return err}defer res.Body.Close()var cmd motor.Comandoif err := json.NewDecoder(res.Body).Decode(&cmd); err != nil { return err}cmd, err = c.Comando(ctx, cmd.ID) // completado o fallidoconst res = await fetch( `https://motor.ejemplo.com/v1/instances/${instanciaId}/calls/${encodeURIComponent(callId)}/reject`, { method: 'POST', headers: { Authorization: `Bearer ${apiKey}` } },);const comando = await res.json(); // 202: { command_id, type: "call.reject", status: "aceptado", … }{ "command_id": "5c2e7a10-0000-4000-8000-0000000000c7", "instance_id": "8a1d2c3e-0000-4000-8000-0000000000a1", "idempotency_key": "rechazo-CALL_ID", "type": "call.reject", "generation": 1, "status": "aceptado", "created_at": "2026-10-24T15:40:05Z", "updated_at": "2026-10-24T15:40:05Z"}{ "call_id": "CALL_ID", "de": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": ["CONTACTO@s.whatsapp.net"] }, "video": false, "rechazo": "app", "momento": "2026-10-24T15:40:06Z"}El sobre de call.rejected trae el command_id del rechazo. Si el rechazo no pudo salir, el
comando termina fallido con command.failed: sin_sesion (la instancia no estaba conectada),
operacion_fallida, sin_vincular o instancia_archivada.
Rechazo automático
Sección titulada «Rechazo automático»Cada instancia puede rechazar sola toda llamada entrante, y opcionalmente mandarle un texto a
quien llamó. Se configura con PATCH /v1/instances/{id} (scope instances:manage):
config.rechazar_llamadas: truelo enciende.config.mensaje_rechazo(hasta 1000 caracteres) es el texto. Si el PATCH no lo trae, no cambia;nullo""lo borra.
Al llegar una oferta nueva, el motor acepta en la misma transacción el rechazo (call.reject) y,
si hay texto, un message.text a quien llamó, por el mismo outbox que cualquier envío. Llega
call.rejected con rechazo: automatico. El texto no se manda en una llamada de grupo, y si la
cola de envíos de la instancia está llena, el rechazo sale igual y el texto se omite.
curl -X PATCH https://motor.ejemplo.com/v1/instances/INSTANCIA_ID \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"config": {"rechazar_llamadas": true, "mensaje_rechazo": "Ahora no puedo atender llamadas. Escríbeme por aquí."}}'// El SDK de Go no trae el PATCH de la instancia: es HTTP directo.cuerpo := strings.NewReader(`{"config": {"rechazar_llamadas": true, "mensaje_rechazo": "Ahora no puedo atender llamadas. Escríbeme por aquí."}}`)req, _ := http.NewRequestWithContext(ctx, http.MethodPatch, c.Base+"/v1/instances/"+instanciaID, cuerpo)req.Header.Set("Authorization", "Bearer "+c.Token)req.Header.Set("Content-Type", "application/json")res, err := c.HTTP.Do(req) // 200 con la instanciaawait fetch(`https://motor.ejemplo.com/v1/instances/${instanciaId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ config: { rechazar_llamadas: true, mensaje_rechazo: 'Ahora no puedo atender llamadas. Escríbeme por aquí.', }, }),}); // 200 con la instanciaPara apagarlo, manda {"config": {"rechazar_llamadas": false, "mensaje_rechazo": ""}}. El cuerpo
es cerrado: un campo de más es 422, y con version_esperada vieja, 409.
Errores comunes
Sección titulada «Errores comunes»| HTTP / estado | Código | Cuándo |
|---|---|---|
| 404 | — | La llamada no existe en esta instancia, o la instancia no es de tu app. |
| 501 | llamadas_no_soportadas |
…/accept u …/offer: el motor no atiende ni hace llamadas. |
| 403 | — | La key no tiene calls:manage. |
| 422 | — | Un PATCH con un campo de más o un mensaje_rechazo que no es texto. |
fallido |
sin_sesion, operacion_fallida, sin_vincular, instancia_archivada |
El rechazo no pudo salir. |
Límites de WhatsApp
Sección titulada «Límites de WhatsApp»Lo comprobado con un iPhone llamando a instancias reales (caso A33, octubre de 2026):
- Rechazo automático: con un texto configurado, la llamada de voz se cortó sola en el iPhone en
menos de 1,5 s desde la oferta, y el texto llegó a quien llamó a los 0,3 s (21/10/2026). Tras
un rechazo del motor no llegó
call.terminate: el rechazo es su final. - Rechazo desde la app o el panel: voz y videollamada se cortaron en el iPhone al rechazar
(22/10/2026). Antes, un rechazo dirigido al alias equivocado (el PN en vez del LID de la oferta)
quedaba
completadoy la llamada seguía sonando; se corrigió para usar el creador exacto de la oferta (falla F33, cerrada el 22/10/2026). La repetición de control por la API después de esa corrección quedó pendiente; antes de ella, el rechazo por la API ya cortaba (21/10/2026), con uncall.terminatede motivorejected_elsewhere. - Videollamadas: el primer intento las marcaba con
video: false; ahoravideosale del elementovideode la oferta y una videollamada llega convideo: true(22/10/2026). - Sin atender: una llamada que nadie atiende termina con
call.terminateymotivo: timeout(a unos 2 minutos en una de las pruebas, 21/10/2026). - Llamadas sin fin: una llamada de la que nunca llegó una señal de fin queda
entranteen la lista. Por esoentrantes=truesolo muestra las de los últimos 2 minutos; rechazar una llamada vieja salecompletadosin efecto en el teléfono.