Grupos y comunidades
Para qué sirve
Sección titulada «Para qué sirve»Con estas rutas tu app maneja los grupos del número vinculado como lo haría un administrador desde el teléfono: crea grupos, agrega y quita participantes, cambia roles, pide y revoca el enlace de invitación, aprueba pedidos para entrar, cambia el nombre, la descripción, la foto y los ajustes, y arma comunidades con sus grupos.
Las consultas responden en el momento. Los cambios son comandos durables: el motor los guarda, responde 202 y los ejecuta contra WhatsApp; el resultado llega en el comando y en el ledger. Lo explica Comandos asíncronos y eventos.
Cada grupo se identifica por su peer_ref en la instancia (el grupo.peer_ref de la lista o de
un evento). Es el {grupo_ref} de las rutas. El mismo grupo visto desde otro número es otra fila,
con otro peer_ref.
Requisitos
Sección titulada «Requisitos»- Una instancia
conectado. Las consultas a WhatsApp (actualizar=true, invitaciones, pedidos para entrar) responden 409no_conectadasin conexión; un comando sin conexión vuelve a la cola y se reintenta. - Una API key con estos scopes:
| Operación | Ruta | Scope |
|---|---|---|
| Listar grupos y comunidades | GET /v1/instances/{id}/groups |
groups:read |
| Ver un grupo y sus participantes | GET /v1/instances/{id}/groups/{grupo_ref} |
groups:read |
| Consultar una invitación sin entrar | POST /v1/instances/{id}/groups/invite/info |
groups:read |
| Ver los pedidos para entrar | GET /v1/instances/{id}/groups/{grupo_ref}/requests |
groups:read |
| Crear un grupo | POST /v1/instances/{id}/groups |
groups:write |
| Agregar, quitar, promover o degradar | POST …/groups/{grupo_ref}/participants |
groups:write |
| Obtener o revocar el enlace | POST …/groups/{grupo_ref}/invite-link |
groups:write |
| Entrar con un enlace | POST /v1/instances/{id}/groups/join |
groups:write |
| Aprobar o rechazar pedidos | POST …/groups/{grupo_ref}/requests |
groups:write |
| Nombre, descripción, foto y ajustes | PUT …/groups/{grupo_ref}/name, /description, /picture, /settings; DELETE …/picture |
groups:write |
| Salir del grupo | POST …/groups/{grupo_ref}/leave |
groups:write |
| Mandar la invitación por mensaje | POST …/groups/{grupo_ref}/invite/send |
groups:read y messages:send |
| Crear una comunidad | POST /v1/instances/{id}/communities |
groups:write |
| Ver una comunidad y sus grupos | GET /v1/instances/{id}/communities/{comunidad_ref} |
groups:read |
| Participantes de la comunidad | GET …/communities/{comunidad_ref}/participants |
groups:read |
| Vincular o desvincular un grupo | POST …/communities/{comunidad_ref}/links |
groups:write |
Para recibir los eventos, además, events:read (ledger o stream) o una suscripción de webhooks.
Todos los comandos piden una Idempotency-Key (de 1 a 200 caracteres): repetir el pedido con
la misma clave devuelve el mismo comando y nunca crea dos grupos.
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 grupos, así que es HTTP directo con el token del cliente.
1. Lista los grupos
Sección titulada «1. Lista los grupos»GET /v1/instances/{id}/groups devuelve lo que el motor sabe de cada grupo, de a limit (1 a 200;
por omisión, 50), con next_cursor y has_more. Con actualizar=true el motor pide antes la
lista a WhatsApp: los grupos de los que el número ya no es parte quedan con miembro: false. Con
comunidades=true devuelve solo las comunidades.
GET …/groups/{grupo_ref} trae la ficha del grupo y sus participantes, cada uno con su rol
(miembro, admin, superadmin) y su telefono cuando el motor tiene comprobado su número (un
LID nunca se convierte en teléfono).
curl "https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/groups?actualizar=true&limit=50" \ -H "Authorization: Bearer TU_API_KEY"
# Un grupo y sus participantes:curl https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/groups/GRUPO_REF \ -H "Authorization: Bearer TU_API_KEY"import motor "github.com/projectx27/motor-whatsapp/sdk/go"
// llamar: el SDK no trae los grupos, 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 lista struct { Grupos []struct { Grupo struct{ PeerRef string `json:"peer_ref"` } `json:"grupo"` Nombre string `json:"nombre"` RolPropio string `json:"rol_propio"` Miembro bool `json:"miembro"` } `json:"grupos"` NextCursor string `json:"next_cursor"` HasMore bool `json:"has_more"`}err := llamar(ctx, c, http.MethodGet, "/v1/instances/"+instanciaID+"/groups?actualizar=true", "", nil, &lista)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 { grupos, next_cursor, has_more } = await llamar( 'GET', `/v1/instances/${instanciaId}/groups?actualizar=true`);{ "grupo": { "grupo": { "peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1", "jid": "GRUPO@g.us", "clase": "grupo", "aliases": [] }, "nombre": "Ventas zona norte", "descripcion": "Coordinación diaria", "anuncio": false, "bloqueado": true, "aprobacion": false, "modo_agregar": "admin_add", "efimero_seg": 0, "es_comunidad": false, "participantes": 3, "miembro": true, "rol_propio": "superadmin", "suspendido": false, "version": 7, "actualizado_en": "2026-10-24T15:30:00Z" }, "participantes": [ { "participante": { "peer_ref": "…", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": ["CONTACTO@s.whatsapp.net"] }, "telefono": "+NUMERO_DEL_CONTACTO", "rol": "admin", "cambio_en": "2026-10-24T15:29:10Z", "desde": "motor" } ]}2. Crea un grupo
Sección titulada «2. Crea un grupo»POST /v1/instances/{id}/groups crea el grupo con el número como superadmin. Solo nombre es
obligatorio (de 1 a 25 caracteres, el límite de WhatsApp).
| Campo | Qué es |
|---|---|
nombre |
De 1 a 25 caracteres. |
participantes |
Hasta 256 destinos (telefono, jid o peer_ref, uno por destino). El número entra solo. |
anuncio |
Solo los administradores escriben. |
bloqueado |
Solo los administradores editan la información. |
aprobacion |
Entrar exige la aprobación de un administrador. |
modo_agregar |
admin_add o all_member_add (de fábrica, todos agregan). |
efimero_seg |
Mensajes temporales: 0, 86400, 604800 o 7776000. |
comunidad |
jid o peer_ref de una comunidad del número: el grupo nace dentro de ella. |
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/groups \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: grupo-ventas-norte" \ -d '{ "nombre": "Ventas zona norte", "participantes": [{"telefono": "+NUMERO_DEL_CONTACTO"}], "bloqueado": true, "modo_agregar": "admin_add" }'// llamar: la función auxiliar del paso 1.var cmd motor.Comandoerr := llamar(ctx, c, http.MethodPost, "/v1/instances/"+instanciaID+"/groups", "grupo-ventas-norte", map[string]any{ "nombre": "Ventas zona norte", "participantes": []map[string]string{{"telefono": "+NUMERO_DEL_CONTACTO"}}, "bloqueado": true, "modo_agregar": "admin_add", }, &cmd)
// Más tarde, el resultado:cmd, err = c.Comando(ctx, cmd.ID) // cmd.Estado == "completado"const cmd = await llamar('POST', `/v1/instances/${instanciaId}/groups`, { clave: 'grupo-ventas-norte', cuerpo: { nombre: 'Ventas zona norte', participantes: [{ telefono: '+NUMERO_DEL_CONTACTO' }], bloqueado: true, modo_agregar: 'admin_add', },}); // 202: { command_id, type: "group.create", status: "aceptado", … }{ "command_id": "5c2e7a10-0000-4000-8000-0000000000c3", "instance_id": "8a1d2c3e-0000-4000-8000-0000000000a1", "idempotency_key": "grupo-ventas-norte", "type": "group.create", "generation": 1, "status": "aceptado", "created_at": "2026-10-24T15:28:00Z", "updated_at": "2026-10-24T15:28:00Z"}Cuando termina, GET /v1/commands/{id} trae status: completado y, en result, el JID del grupo
y el resultado de cada participante tal como lo dio WhatsApp, el propio número incluido.
codigo 0 es que entró; un 403 es que su privacidad no deja agregarlo y viene una invitacion
con su vencimiento; un 409, que ya estaba.
{ "grupo": { "jid": "GRUPO@g.us" }, "participantes": [ { "jid": "TU_NUMERO@s.whatsapp.net", "codigo": 0, "rol": "superadmin" }, { "jid": "CONTACTO@s.whatsapp.net", "codigo": 0, "rol": "miembro" }, { "jid": "OTRO_CONTACTO@s.whatsapp.net", "codigo": 403, "rol": "miembro", "invitacion": "CODIGO_DE_INVITACION", "invitacion_vence": "2026-10-27T15:28:01Z" } ], "hechos": 2, "fallidos": 1}3. Participantes y roles
Sección titulada «3. Participantes y roles»POST …/groups/{grupo_ref}/participants aplica una accion a hasta 50 participantes: add
(agregar), remove (quitar), promote (hacer administrador) o demote (quitarle el rol). El
result trae el resultado de cada uno (codigo 0: hecho; si no, el código del servidor), con
hechos y fallidos, y el ledger recibe un group.participants con lo que cambió.
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/groups/GRUPO_REF/participants \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ventas-norte-promover-1" \ -d '{"accion": "promote", "participantes": [{"peer_ref": "4b7d9e01-0000-4000-8000-0000000000b2"}]}'// llamar: la función auxiliar del paso 1.var cmd motor.Comandoerr := llamar(ctx, c, http.MethodPost, "/v1/instances/"+instanciaID+"/groups/"+grupoRef+"/participants", "ventas-norte-promover-1", map[string]any{ "accion": "promote", "participantes": []map[string]string{{"peer_ref": contactoRef}}, }, &cmd)await llamar('POST', `/v1/instances/${instanciaId}/groups/${grupoRef}/participants`, { clave: 'ventas-norte-promover-1', cuerpo: { accion: 'promote', participantes: [{ peer_ref: contactoRef }] },});4. Invitaciones
Sección titulada «4. Invitaciones»El enlace de invitación es un comando: POST …/groups/{grupo_ref}/invite-link (solo un
administrador). Su result.enlace trae https://chat.whatsapp.com/…, y el motor guarda el último
en la ficha del grupo (invitacion). Con {"revocar": true} el enlace vigente deja de servir y
WhatsApp da uno nuevo.
Con un enlace (el tuyo o el de otro número):
- Consultarlo sin entrar:
POST …/groups/invite/infoconenlace(el enlace o solo su código) responde en el momento connombre,descripcion,participantes,aprobacion,es_comunidadycreado_en. Un enlace inválido es 404invitacion_invalida; revocado o vencido, 410invitacion_revocada. - Entrar:
POST …/groups/joinconenlacees un comandogroup.join. Suresulttrae el JID del grupo ypendiente_aprobacion: truesi el grupo exige que un administrador apruebe la entrada. Si el número ya estaba, falla conya_es_miembro. - Mandarlo por mensaje:
POST …/groups/{grupo_ref}/invite/sendmanda el último enlace que obtuvo el motor a hasta 20destinos, como un texto por destino (contextodelante, hasta 1000 caracteres). No inscribe a nadie: cada uno decide si entra. Responde con elenlacey un comando por destino, con laIdempotency-Keymás:0,:1… (por eso esta clave admite hasta 190 caracteres). Si el motor todavía no pidió el enlace, 409sin_enlace.
# Pide el enlace (comando):curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/groups/GRUPO_REF/invite-link \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ventas-norte-enlace-1" \ -d '{}'
# Consulta un enlace sin entrar:curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/groups/invite/info \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -d '{"enlace": "https://chat.whatsapp.com/CODIGO_DE_INVITACION"}'
# Entra con el enlace (comando):curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/groups/join \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: unirse-proveedores" \ -d '{"enlace": "https://chat.whatsapp.com/CODIGO_DE_INVITACION"}'// llamar: la función auxiliar del paso 1.ruta := "/v1/instances/" + instanciaID + "/groups"
var cmd motor.Comandoerr := llamar(ctx, c, http.MethodPost, ruta+"/"+grupoRef+"/invite-link", "ventas-norte-enlace-1", map[string]any{}, &cmd)
var info struct { Nombre string `json:"nombre"` Participantes int `json:"participantes"` Aprobacion bool `json:"aprobacion"`}err = llamar(ctx, c, http.MethodPost, ruta+"/invite/info", "", map[string]string{"enlace": enlace}, &info)
err = llamar(ctx, c, http.MethodPost, ruta+"/join", "unirse-proveedores", map[string]string{"enlace": enlace}, &cmd)const ruta = `/v1/instances/${instanciaId}/groups`;
await llamar('POST', `${ruta}/${grupoRef}/invite-link`, { clave: 'ventas-norte-enlace-1', cuerpo: {},});
const info = await llamar('POST', `${ruta}/invite/info`, { cuerpo: { enlace } });// { nombre, participantes, aprobacion, es_comunidad, … }
await llamar('POST', `${ruta}/join`, { clave: 'unirse-proveedores', cuerpo: { enlace } });5. Pedidos para entrar
Sección titulada «5. Pedidos para entrar»En un grupo con aprobacion, quien entra con el enlace queda pendiente.
GET …/groups/{grupo_ref}/requests consulta los pedidos a WhatsApp en el momento (solo un
administrador; si no, 403 sin_permiso): cada uno trae jid, telefono cuando se conoce y
momento. POST …/groups/{grupo_ref}/requests los resuelve con accion approve o reject
para hasta 50 participantes; el result trae el resultado de cada uno y los que entran llegan en
un group.participants.
curl https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/groups/GRUPO_REF/requests \ -H "Authorization: Bearer TU_API_KEY"
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/groups/GRUPO_REF/requests \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ventas-norte-aprobar-1" \ -d '{"accion": "approve", "participantes": [{"jid": "CONTACTO@lid"}]}'// llamar: la función auxiliar del paso 1.ruta := "/v1/instances/" + instanciaID + "/groups/" + grupoRef + "/requests"
var pedidos struct { Solicitudes []struct { JID string `json:"jid"` Momento time.Time `json:"momento"` } `json:"solicitudes"`}err := llamar(ctx, c, http.MethodGet, ruta, "", nil, &pedidos)
var cmd motor.Comandoerr = llamar(ctx, c, http.MethodPost, ruta, "ventas-norte-aprobar-1", map[string]any{ "accion": "approve", "participantes": []map[string]string{{"jid": pedidos.Solicitudes[0].JID}},}, &cmd)const ruta = `/v1/instances/${instanciaId}/groups/${grupoRef}/requests`;const { solicitudes } = await llamar('GET', ruta);
await llamar('POST', ruta, { clave: 'ventas-norte-aprobar-1', cuerpo: { accion: 'approve', participantes: [{ jid: solicitudes[0].jid }] },});6. Nombre, descripción, foto y ajustes
Sección titulada «6. Nombre, descripción, foto y ajustes»Cada cambio es un comando con su Idempotency-Key:
| Cambio | Ruta | Cuerpo | Tipo de comando |
|---|---|---|---|
| Nombre | PUT …/groups/{grupo_ref}/name |
{"nombre": "…"} (1 a 25 caracteres) |
group.name |
| Descripción | PUT …/groups/{grupo_ref}/description |
{"texto": "…"} (hasta 2048; vacío la borra) |
group.description |
| Foto | PUT …/groups/{grupo_ref}/picture |
{"media_id": "…"} |
group.picture |
| Quitar la foto | DELETE …/groups/{grupo_ref}/picture |
sin cuerpo | group.picture |
| Un ajuste | PUT …/groups/{grupo_ref}/settings |
{"ajuste": "…", "valor": true} |
group.setting |
| Salir | POST …/groups/{grupo_ref}/leave |
sin cuerpo | group.leave |
La foto es un JPEG que subiste antes a esta instancia con POST /v1/instances/{id}/media/uploads
(ver Multimedia), ya listo, de hasta 2 MB y de 192 a 2048 píxeles por lado: el
motor la recorta al centro y la deja en 640 × 640, la medida que acepta WhatsApp.
ajuste es uno por comando:
ajuste |
Qué hace |
|---|---|
announce |
Con valor: true, solo los administradores escriben. |
locked |
Con valor: true, solo los administradores editan la información. |
approval |
Con valor: true, entrar exige la aprobación de un administrador. |
member_add |
Quién agrega participantes: va con modo (admin_add o all_member_add), sin valor. |
curl -X PUT https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/groups/GRUPO_REF/settings \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ventas-norte-anuncio-on" \ -d '{"ajuste": "announce", "valor": true}'
curl -X PUT https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/groups/GRUPO_REF/name \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: ventas-norte-nombre-2" \ -d '{"nombre": "Ventas norte 2026"}'// llamar: la función auxiliar del paso 1.ruta := "/v1/instances/" + instanciaID + "/groups/" + grupoRef
var cmd motor.Comandoerr := llamar(ctx, c, http.MethodPut, ruta+"/settings", "ventas-norte-anuncio-on", map[string]any{"ajuste": "announce", "valor": true}, &cmd)
err = llamar(ctx, c, http.MethodPut, ruta+"/settings", "ventas-norte-agregan-admins", map[string]any{"ajuste": "member_add", "modo": "admin_add"}, &cmd)const ruta = `/v1/instances/${instanciaId}/groups/${grupoRef}`;
await llamar('PUT', `${ruta}/settings`, { clave: 'ventas-norte-anuncio-on', cuerpo: { ajuste: 'announce', valor: true },});
await llamar('PUT', `${ruta}/name`, { clave: 'ventas-norte-nombre-2', cuerpo: { nombre: 'Ventas norte 2026' },});7. Comunidades
Sección titulada «7. Comunidades»Una comunidad es un grupo con es_comunidad: true que agrupa otros grupos.
- Crearla:
POST /v1/instances/{id}/communitiesconnombre(1 a 25 caracteres) y, si quieres,descripcion(hasta 2048) yaprobacion. WhatsApp crea solo su grupo de anuncios (subgrupo_por_defecto: true), que el motor lee y deja vinculado. Elresulttrae el JID de la comunidad. Como un grupo, si pudo salir y no hubo respuesta, quedaambiguo. - Un grupo nuevo dentro:
POST …/groupsconcomunidad(hay que administrarla). - Vincular o desvincular un grupo existente:
POST …/communities/{comunidad_ref}/linkscongrupo(jidopeer_ref) yvincular(trueofalse). El número tiene que administrar la comunidad. Después el motor lee los grupos de la comunidad y concilia la lista: un vínculo que ya estaba no es un error ni se duplica. - Verla:
GET …/communities/{comunidad_ref}trae la comunidad y sussubgrupos, cada uno conanuncios,conocido(el número está en ese grupo) y laversionde la relación. Conactualizar=trueel motor pide la lista a WhatsApp y la concilia. Si esepeer_refno es una comunidad, 422no_es_comunidad. - Sus participantes:
GET …/communities/{comunidad_ref}/participantslos consulta a WhatsApp en el momento (jidytelefono); no se guardan.
El {comunidad_ref} es el peer_ref de la comunidad (GET …/groups?comunidades=true).
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/communities \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: comunidad-sucursales" \ -d '{"nombre": "Sucursales", "descripcion": "Todas las sucursales"}'
curl -X POST https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/communities/COMUNIDAD_REF/links \ -H "Authorization: Bearer TU_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: sucursales-vincular-norte" \ -d '{"grupo": {"peer_ref": "4b7d9e01-0000-4000-8000-0000000000b1"}, "vincular": true}'
curl "https://motor.ejemplo.com/v1/instances/INSTANCIA_ID/communities/COMUNIDAD_REF?actualizar=true" \ -H "Authorization: Bearer TU_API_KEY"// llamar: la función auxiliar del paso 1.ruta := "/v1/instances/" + instanciaID + "/communities"
var cmd motor.Comandoerr := llamar(ctx, c, http.MethodPost, ruta, "comunidad-sucursales", map[string]any{"nombre": "Sucursales", "descripcion": "Todas las sucursales"}, &cmd)
err = llamar(ctx, c, http.MethodPost, ruta+"/"+comunidadRef+"/links", "sucursales-vincular-norte", map[string]any{"grupo": map[string]string{"peer_ref": grupoRef}, "vincular": true}, &cmd)
var vista struct { Subgrupos []struct { Nombre string `json:"nombre"` Anuncios bool `json:"anuncios"` } `json:"subgrupos"`}err = llamar(ctx, c, http.MethodGet, ruta+"/"+comunidadRef+"?actualizar=true", "", nil, &vista)const ruta = `/v1/instances/${instanciaId}/communities`;
await llamar('POST', ruta, { clave: 'comunidad-sucursales', cuerpo: { nombre: 'Sucursales', descripcion: 'Todas las sucursales' },});
await llamar('POST', `${ruta}/${comunidadRef}/links`, { clave: 'sucursales-vincular-norte', cuerpo: { grupo: { peer_ref: grupoRef }, vincular: true },});
const { comunidad, subgrupos } = await llamar('GET', `${ruta}/${comunidadRef}?actualizar=true`);El comando y sus eventos
Sección titulada «El comando y sus eventos»Todos los cambios nacen aceptado y terminan completado o fallido (o ambiguo, en una
creación). Los tipos de comando son group.create, group.leave, group.name,
group.description, group.picture, group.invite_link, group.join, group.participants,
group.setting, group.requests, community.create y community.link; mandar la invitación es
group.invite_send, y cada mensaje que sale de él, un message.text.
El efecto llega al ledger con el command_id del comando y desde: motor. Lo que cambia desde un
teléfono llega con desde: whatsapp, y lo que el motor descubre al consultar, con
desde: consulta. Cada evento trae el grupo como peer.
group.changed
Sección titulada «group.changed»Cambió la ficha del grupo. campos trae solo lo que cambió, con su valor nuevo: nombre,
descripcion, anuncio, bloqueado, aprobacion, modo_agregar, efimero_seg, suspendido,
foto_id, invitacion (solo true: el enlace no viaja en el evento), eliminado, miembro,
rol_propio, es_comunidad o comunidad. autor llega cuando WhatsApp dice quién lo hizo.
{ "grupo": { "peer_ref": "…", "jid": "GRUPO@g.us", "clase": "grupo", "aliases": [] }, "campos": { "nombre": "Ventas norte 2026" }, "desde": "motor"}Si una consulta con actualizar=true ya no ve un grupo, llega un group.changed con
campos: {"miembro": false} y desde: consulta.
group.participants
Sección titulada «group.participants»Entraron, salieron o cambiaron de rol participantes. Las listas sin movimiento llegan vacías o en
null; motivo es el que informa WhatsApp y puede venir vacío. Está en el
Catálogo de eventos.
{ "grupo": { "peer_ref": "…", "jid": "GRUPO@g.us", "clase": "grupo", "aliases": [] }, "desde": "motor", "entran": null, "salen": null, "promovidos": [{ "peer_ref": "…", "jid": "CONTACTO@lid", "clase": "usuario", "aliases": [] }], "degradados": null, "motivo": ""}group.joined
Sección titulada «group.joined»El número entró a un grupo: lo creó, lo agregaron o entró con un enlace. tipo y motivo son los
que informa WhatsApp (tipo: "new" cuando el número lo creó); participantes es la cantidad y
autor llega si WhatsApp dice quién lo agregó. Una creación deja un solo group.joined.
{ "grupo": { "peer_ref": "…", "jid": "GRUPO@g.us", "clase": "grupo", "aliases": [] }, "nombre": "Ventas zona norte", "tipo": "new", "motivo": "", "participantes": 2, "es_comunidad": false, "desde": "motor"}community.changed
Sección titulada «community.changed»Un grupo se vinculó o se desvinculó de una comunidad. anuncios dice si es su grupo de anuncios y
version es la versión de la relación. Cuando un grupo pasa a otra comunidad, la salida de la
anterior llega con motivo: "otra_comunidad".
{ "comunidad": { "peer_ref": "…", "jid": "COMUNIDAD@g.us", "clase": "grupo", "aliases": [] }, "grupo": { "peer_ref": "…", "jid": "GRUPO@g.us", "clase": "grupo", "aliases": [] }, "nombre": "Ventas zona norte", "vinculado": true, "anuncios": false, "version": 2, "desde": "motor"}Los mensajes de un grupo no son eventos de este dominio: llegan como message.received con
es_grupo: true y autor (ver Mensajes).
Errores comunes
Sección titulada «Errores comunes»Las consultas responden con un error HTTP; el error_code va en type como
urn:motor-whatsapp:error:<código> cuando el error es tipado. Los comandos, en cambio, se aceptan
con 202 y fallan después con error_code en el comando y en command.failed.
| HTTP | error_code |
Cuándo |
|---|---|---|
| 403 | — | A tu key le falta el scope de la operación. |
| 403 | sin_permiso |
Ver los pedidos para entrar sin ser administrador. |
| 404 | — | La instancia no conoce ese grupo: actualiza la lista con actualizar=true. |
| 404 | invitacion_invalida |
El enlace no es válido. |
| 409 | no_conectada |
Una consulta a WhatsApp con la instancia sin conexión. |
| 409 | sin_vincular |
La instancia no está vinculada a un número. |
| 409 | sin_enlace |
Mandar la invitación antes de pedir el enlace. |
| 409 | — | La misma Idempotency-Key con otros datos. |
| 410 | invitacion_revocada |
El enlace fue revocado o venció. |
| 422 | — | Un cuerpo inválido: nombre de más de 25 caracteres, member_add sin modo, un efimero_seg que no es 0, 86400, 604800 ni 7776000, el grupo sin jid ni peer_ref. |
| 422 | foto_invalida |
La foto no es un JPEG listo de esta instancia, pesa más de 2 MB o no mide de 192 a 2048 píxeles por lado. |
| 422 | no_es_comunidad |
Ese peer_ref no es una comunidad. |
Los error_code de un comando que falla:
error_code |
Qué pasó |
|---|---|
sin_permiso |
El número no es administrador. WhatsApp responde con un 401 del servidor (server_code: 401, server_origin: iq). |
no_es_miembro |
El número no está en ese grupo. |
grupo_no_encontrado |
WhatsApp dice que el grupo no existe. |
invitacion_invalida, invitacion_revocada |
Entrar con un enlace inválido, revocado o vencido. |
ya_es_miembro |
Entrar con un enlace a un grupo en el que el número ya está. |
foto_invalida |
La foto no se pudo usar. |
rechazado_por_whatsapp |
WhatsApp rechazó el pedido; server_code trae su código. |
resultado_incierto |
Una creación sin respuesta: el comando queda ambiguo. |
limitado_por_whatsapp |
WhatsApp está limitando la operación; el motor reintenta y, si sigue, falla. |
Límites de WhatsApp
Sección titulada «Límites de WhatsApp»- Vincular un grupo a una comunidad puede quedar limitado por WhatsApp un par de horas (caso
A63, 17/10/2026): el motor reintentó tres veces con
limitado_por_whatsappy el teléfono tampoco dejaba. Está en Limitaciones de WhatsApp. La mudanza completa de un grupo entre dos comunidades no se pudo probar; la salida de la comunidad anterior sí llega comocommunity.changedconvinculado: false. - El rol lo pone WhatsApp (caso A57, 21/10/2026): sin ser administrador, cambiar el nombre de
un grupo con
bloqueadofalla consin_permiso(el servidor responde 401); hecho administrador desde el teléfono, el mismo cambio pasa. - La privacidad de cada contacto manda (caso A56, 21/10/2026): un contacto con «Quién puede
agregarme a grupos» en «Mis contactos» vuelve con
codigo: 403y un código deinvitacionque vence a los 3 días. Mándale el enlace con…/invite/send. - Invitaciones y aprobación (caso A58, 21/10/2026): el enlace revocado da 410; un pedido
para entrar aparece en
…/requestscon eljidcomo@lid, y aprobarlo desde el motor deja ungroup.participantsdesde: motor. - Crear con la red cortada (caso A62, 16/10/2026): el comando quedó
ambiguo(resultado_incierto) a los 60 s y, al volver la red, no se mandó otra vez: no apareció ningún grupo duplicado. - Comunidades (caso A59, 21/10/2026): crear una comunidad, con su grupo de anuncios y la
descripción, y un grupo dentro, pasa. La forma del pedido a WhatsApp es un ajuste del operador
(
grupos.forma_comunidad; de fábrica,minima) y queda fija al aceptar el comando. - Desvincular desde el teléfono (caso A60, 17/10/2026): llega
community.changedconvinculado: falseydesde: whatsapp, y la lista de la comunidad se concilia sin duplicados.