Estados
Para qué sirve
Sección titulada «Para qué sirve»Los estados (status, las historias de 24 horas) se publican y se reciben en el chat especial
status@broadcast. Con estas rutas tu app:
- prueba la audiencia de estados del número, sin publicar nada;
- publica un estado de texto, imagen o vídeo declarando a quién sale;
- consulta los estados que el motor todavía retiene: los de tus contactos y los del número;
- recibe
status.receivedcuando un contacto publica ystatus.revokedcuando lo borra.
Requisitos
Sección titulada «Requisitos»- Una instancia
conectado. Probar la audiencia responde 409no_conectadasin conexión. - Una API key con estos scopes:
| Operación | Ruta | Scope |
|---|---|---|
| Probar la audiencia | GET /v1/instances/{id}/status/audience |
status:read |
| Listar los estados retenidos | GET /v1/instances/{id}/status |
status:read |
| Publicar un estado | POST /v1/instances/{id}/status |
status:write |
Para recibir los eventos, además, events:read o una suscripción de webhooks. Para publicar una
imagen o un vídeo, súbelo antes con media:write (ver Multimedia).
Los ejemplos de Go y JavaScript usan una función auxiliar, llamar, que se define en el primer
paso: el SDK de Go no trae métodos de estados, así que es HTTP directo con el token del cliente.
1. Prueba la audiencia
Sección titulada «1. Prueba la audiencia»GET /v1/instances/{id}/status/audience pregunta a WhatsApp la privacidad de estados del número
(la que se elige en el teléfono) y cuenta su audiencia. Es lo que vas a declarar al publicar.
| Campo | Qué es |
|---|---|
privacidad |
contacts (mis contactos), allowlist (solo compartir con…) o denylist (mis contactos excepto…). |
destinatarios |
Cuántos contactos tiene esa audiencia ahora (sin contar a quien no usa WhatsApp). |
max_destinatarios_ajustes |
Lo más que puede declarar una publicación (Ajustes del operador; de fábrica, 256). |
horas_retencion |
Cuánto guarda el motor un estado (de 1 a 24; de fábrica, 24). |
curl https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/status/audience \ -H "Authorization: Bearer TU_API_KEY"import motor "github.com/projectx27/motor-whatsapp/sdk/go"
// llamar: el SDK no trae los estados, así que es HTTP directo con el token del cliente.// clave va como Idempotency-Key (vacía en las consultas).func llamar(ctx context.Context, c *motor.Cliente, metodo, ruta, clave string, cuerpo, destino any) error { var r io.Reader if cuerpo != nil { b, err := json.Marshal(cuerpo) if err != nil { return err } r = bytes.NewReader(b) } req, err := http.NewRequestWithContext(ctx, metodo, c.Base+ruta, r) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+c.Token) if cuerpo != nil { req.Header.Set("Content-Type", "application/json") } if clave != "" { req.Header.Set("Idempotency-Key", clave) } res, err := c.HTTP.Do(req) if err != nil { return err } defer res.Body.Close() if res.StatusCode >= 300 { return fmt.Errorf("motor: %d", res.StatusCode) } if destino == nil { return nil } return json.NewDecoder(res.Body).Decode(destino)}
// En tu código:c := motor.Nuevo("https://motor.ejemplo.com", apiKey)var audiencia struct { Privacidad string `json:"privacidad"` Destinatarios int `json:"destinatarios"` Maximo int `json:"max_destinatarios_ajustes"`}err := llamar(ctx, c, http.MethodGet, "/v1/instances/"+instanciaID+"/status/audience", "", nil, &audiencia)const base = 'https://motor.ejemplo.com';
// llamar: una petición a /v1 con el token; clave va como Idempotency-Key.async function llamar(metodo, ruta, { clave, cuerpo } = {}) { const headers = { Authorization: `Bearer ${apiKey}` }; if (cuerpo !== undefined) headers['Content-Type'] = 'application/json'; if (clave) headers['Idempotency-Key'] = clave; const res = await fetch(base + ruta, { method: metodo, headers, body: cuerpo === undefined ? undefined : JSON.stringify(cuerpo), }); const datos = await res.json().catch(() => null); if (!res.ok) throw new Error(`${res.status} ${datos?.type ?? ''} ${datos?.detail ?? ''}`); return datos;}
const audiencia = await llamar('GET', `/v1/instances/${instanciaId}/status/audience`);{ "privacidad": "contacts", "destinatarios": 42, "max_destinatarios_ajustes": 256, "horas_retencion": 24}2. Publica con la audiencia explícita
Sección titulada «2. Publica con la audiencia explícita»POST /v1/instances/{id}/status va por el mismo outbox que un mensaje: la Idempotency-Key es
obligatoria y responde 202 con el comando en_cola.
| Campo | Qué es |
|---|---|
tipo |
text (por omisión), image o video. Otros tipos, como una ubicación o un audio, no se publican como estado. |
texto |
Hasta 700 caracteres: el texto (obligatorio en text) o el pie de la imagen o el vídeo. |
fondo |
Solo en text: el color de fondo, #RRGGBB. |
medio |
Solo en image y video: media_id (una subida de esta instancia, lista) o url (un host de los destinos permitidos). |
audiencia |
Obligatoria. Ver abajo. |
expires_at, correlation_id |
Como en un mensaje: si no salió antes de expires_at, no sale. |
La audiencia dice a quién sale y nunca se adivina:
| Campo | Qué es |
|---|---|
modo |
privacidad: la audiencia de la privacidad de estados del número. lista: solo los destinos. |
privacidad |
En modo: privacidad, la que esperas (contacts, allowlist o denylist). Si la del número es otra, no sale. |
destinos |
En modo: lista, de 1 a max_destinatarios usuarios (telefono, jid o peer_ref). Tienen que estar dentro de la privacidad del número. |
max_destinatarios |
Obligatorio, de 1 a 5000: lo más que aceptas. Si la audiencia tiene más, no sale. No puede pasar el máximo de Ajustes. |
Un estado no lleva cita, menciones ni vista previa.
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/status \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: estado-horario-feriado" \ -d '{ "tipo": "text", "texto": "El lunes feriado abrimos de 10 a 14 h.", "fondo": "#1F6FEB", "audiencia": {"modo": "privacidad", "privacidad": "contacts", "max_destinatarios": 256} }'
# Solo a dos contactos, con una imagen:curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/status \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: estado-catalogo-mayoristas" \ -d '{ "tipo": "image", "medio": {"media_id": "8a1d2c3e-0000-4000-8000-0000000000e2"}, "texto": "Nuevo catálogo para mayoristas", "audiencia": { "modo": "lista", "destinos": [{"telefono": "+NUMERO_DEL_CONTACTO"}, {"peer_ref": "4b7d9e01-0000-4000-8000-0000000000b3"}], "max_destinatarios": 2 } }'// llamar: la función auxiliar del paso 1.var cmd motor.Comandoerr := llamar(ctx, c, http.MethodPost, "/v1/instances/"+instanciaID+"/status", "estado-horario-feriado", map[string]any{ "tipo": "text", "texto": "El lunes feriado abrimos de 10 a 14 h.", "fondo": "#1F6FEB", "audiencia": map[string]any{ "modo": "privacidad", "privacidad": audiencia.Privacidad, // la que dio el paso 1 "max_destinatarios": 256, }, }, &cmd)
// Más tarde:cmd, err = c.Comando(ctx, cmd.ID) // cmd.Estado == "enviado"const cmd = await llamar('POST', `/v1/instances/${instanciaId}/status`, { clave: 'estado-horario-feriado', cuerpo: { tipo: 'text', texto: 'El lunes feriado abrimos de 10 a 14 h.', fondo: '#1F6FEB', audiencia: { modo: 'privacidad', privacidad: audiencia.privacidad, max_destinatarios: 256 }, },}); // 202: { command_id, type: "message.status_text", status: "en_cola", … }Cuando sale, el comando queda enviado y su result dice la audiencia real:
{ "provider_message_id": "PROVIDER_MESSAGE_ID", "vence_en": "2026-10-25T16:30:00Z", "audiencia_conocida": true, "privacidad": "contacts", "destinatarios": 41, "omitidos": ["CONTACTO@s.whatsapp.net"], "sin_cifrar": [], "parcial": true}omitidos: quienes no tienen dispositivos (no usan WhatsApp).sin_cifrar: quienes no recibieron su copia cifrada en ningún dispositivo; no lo pueden ver y no cuentan como destinatarios.parcial: hubo omitidos o sin cifrar.vence_en: cuándo vence en WhatsApp (24 horas).
Si un estado quedó ambiguo (pudo salir sin respuesta) y después llega su eco o un recibo, pasa a
enviado con su vence_en. Si el motor no sabe a quién salió, audiencia_conocida es false y no
hay destinatarios: nunca un cero inventado.
3. Consulta los estados retenidos
Sección titulada «3. Consulta los estados retenidos»GET /v1/instances/{id}/status devuelve los estados que el motor todavía guarda, los más nuevos
primero, de a limit (1 a 200; por omisión, 50), con next_cursor y has_more. Con
direccion=entrante solo los de tus contactos; con saliente, los del número.
Cada uno trae status_id, provider_id, autor (en los entrantes), tipo, texto (el texto o el
pie), mime, momento, vence_en, retener_hasta, revocado y, si lo publicó el motor, su
command_id. El archivo de un estado recibido no se descarga: solo su tipo, su MIME y su pie.
curl "https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/status?direccion=entrante&limit=50" \ -H "Authorization: Bearer TU_API_KEY"// llamar: la función auxiliar del paso 1.var pagina struct { Estados []struct { StatusID string `json:"status_id"` Tipo string `json:"tipo"` Texto string `json:"texto"` VenceEn time.Time `json:"vence_en"` Revocado bool `json:"revocado"` } `json:"estados"` NextCursor string `json:"next_cursor"` HasMore bool `json:"has_more"`}err := llamar(ctx, c, http.MethodGet, "/v1/instances/"+instanciaID+"/status?direccion=entrante", "", nil, &pagina)const { estados, next_cursor, has_more } = await llamar( 'GET', `/v1/instances/${instanciaId}/status?direccion=entrante`);Retención
Sección titulada «Retención»Un estado vence en WhatsApp a las 24 horas. El motor lo guarda hasta retener_hasta: las horas de
estados.horas_retencion (Ajustes del operador, de 1 a 24; de fábrica, 24), nunca más que su
vencimiento. Después lo borra de la lista y el contenido de su status.received queda redactado
(data: {"motivo": "retencion", "redactado": true}). Vencer no produce ningún evento: no hay un
status.expired. Un estado que llega cuando ya pasó su retención no se guarda.
El comando y sus eventos
Sección titulada «El comando y sus eventos»Publicar crea un envío del tipo message.status_text, message.status_image o
message.status_video, que nace en_cola y termina enviado (con message.sent), fallido o
ambiguo, como un mensaje (ver Comandos asíncronos y eventos). Un
estado publicado por el motor no produce status.received: su hecho es el message.sent.
status.received
Sección titulada «status.received»Un contacto publicó un estado (o el número publicó uno desde el teléfono, con from_me: true).
tipo es el del contenido (texto, imagen, video…); texto es el texto o el pie.
{ "status_id": "7d3c1b20-0000-4000-8000-0000000000f1", "autor": { "peer_ref": "…", "jid": "CONTACTO@s.whatsapp.net", "clase": "usuario", "aliases": [] }, "provider_id": "PROVIDER_MESSAGE_ID", "from_me": false, "tipo": "imagen", "texto": "Nueva temporada", "mime": "image/jpeg", "vence_en": "2026-10-25T16:40:00Z", "retener_hasta": "2026-10-25T16:40:00Z"}status.revoked
Sección titulada «status.revoked»Quien lo publicó lo borró. El estado queda revocado, sin contenido, y su status.received se
redacta con motivo: "revocado". Llega una sola vez.
{ "status_id": "7d3c1b20-0000-4000-8000-0000000000f1", "autor": { "peer_ref": "…", "jid": "CONTACTO@s.whatsapp.net", "clase": "usuario", "aliases": [] }, "provider_id": "PROVIDER_MESSAGE_ID", "from_me": false}Errores comunes
Sección titulada «Errores comunes»La publicación se valida antes de guardarse (errores HTTP) y otra vez antes de salir (el comando
termina fallido sin que salga nada).
| HTTP | error_code |
Cuándo |
|---|---|---|
| 403 | — | A tu key le falta status:write o status:read. |
| 409 | no_conectada |
Probar la audiencia con la instancia sin conexión. |
| 409 | — | La misma Idempotency-Key con otro estado. |
| 422 | — | Un estado de texto sin texto o con medio; uno de imagen o vídeo sin medio o con fondo. |
| 422 | audiencia_invalida |
modo: privacidad sin privacidad o con destinos; modo: lista sin destinos o con más de max_destinatarios; un fondo que no es #RRGGBB. |
| 422 | audiencia_maxima |
max_destinatarios pasa el máximo de Ajustes. |
| 422 | no_en_status |
Un estado con cita, menciones o vista previa. |
| 429 | — | El ritmo de la instancia o la cola llena, como en un mensaje. |
Los error_code de un estado que no salió:
error_code |
Qué pasó |
|---|---|
privacidad_distinta |
La privacidad de estados del número no es la que declaraste. |
privacidad_desconocida |
WhatsApp informó una privacidad que el motor no conoce. |
fuera_de_privacidad |
En modo: lista, algún destino no está dentro de la privacidad del número. |
audiencia_excedida |
La audiencia tiene más que tu max_destinatarios. |
sin_destinatarios |
No hay nadie a quien mandarlo. |
sin_cifrar |
No se pudo cifrar la copia de nadie de la audiencia. |
comando_vencido |
Pasó expires_at antes de salir. |
Límites de WhatsApp
Sección titulada «Límites de WhatsApp»- No se puede borrar un estado propio por la API (caso A69, 21/10/2026): no hay ruta para eso; bórralo desde el teléfono o espera las 24 horas. Ver Otros límites observados.
- El borrado solo llega a los contactos actuales (caso A69, 22/10/2026): borrado desde el
teléfono, el estado llegó como
status.revokeda la otra instancia, lo hubiera publicado el motor o el teléfono. Pero si el contacto ya no estaba en la libreta de quien publicó, WhatsApp no le avisó del borrado: el estado quedó hasta vencer. - La privacidad de estados se cambia en el teléfono (caso A67, 21 y 22/10/2026): la API no la
cambia. Con la privacidad en «Solo compartir con…», declarar
contactsfalló conprivacidad_distintaa los 0,1 s, sin salir nada. - Una lista explícita (caso A68, 21/10/2026): un estado de imagen con
modo: listay un solo contacto salióenviadocondestinatarios: 1y solo lo vio ese contacto. - Publicar con la red cortada (caso A70, 22/10/2026): el estado no se publicó dos veces; al
reconectar salió una sola vez con su
vence_eny su audiencia. El casoambiguoconciliado por un recibo no se pudo provocar en real.