Seguir quién entra y sale de tus grupos
Recibí un aviso firmado cada vez que alguien entra o sale de un grupo de tus campañas, cargá los participantes actuales y recuperá los movimientos que te perdiste.
Vas a llevar en tu sistema quién está en cada grupo de WhatsApp de tus campañas. Usa los eventos group_participant.joined y group_participant.left del área de grupos, con Crear un webhook y Enviar un evento de prueba, y dos lecturas: Listar los participantes de un grupo para la foto actual y Listar entradas y salidas de un grupo para el historial.
Al terminar esta guía, cada entrada y cada salida llega a tu servidor como un POST firmado con el grupo, la campaña y la persona, y tenés cómo cargar la lista inicial y completar lo que tu receptor no recibió.
Antes de empezar
- Una clave con
communities:readywebhooks:write.communities:readlee los grupos y sus participantes, y es además el permiso del área de los eventos de grupos, que la suscripción pide;webhooks:writecrea y prueba la suscripción. Si todavía no tenés una, seguí la guía rápida. - La clave en la variable
FC_API_KEY, entre comillas si la guardás en un.env(lleva un|). - Un receptor con URL
httpspública. El sobre, las cabeceras y la firma son los mismos para todos los eventos: podés partir del receptor de Recibir los mensajes de WhatsApp en tu sistema. Si ya tenés una suscripción amessage.received, podés sumarle estos eventos conPUTen lugar de crear otra; si preferís otra suscripción, tiene su propio secreto: usá una URL distinta para cada una y verificá cada una con su secreto.
1. Suscribite a las entradas y salidas
Creá la suscripción en /api/v1/webhooks, el mismo lugar para los eventos de las tres áreas. Pide webhooks:write y, por ser eventos de grupos, communities:read.
curl -X POST "https://api.funnelchat.com/api/v1/webhooks" \
-H "Authorization: Bearer $FC_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tu-servidor.example/hooks/funnelchat-grupos",
"description": "Entradas y salidas de grupos",
"events": ["group_participant.joined", "group_participant.left"]
}'La respuesta (201) es la suscripción, sin envoltorio data. Estos son sus campos según la especificación; los valores son de ejemplo:
{
"id": "00000000-0000-4000-8000-0000000000a1",
"url": "https://tu-servidor.example/hooks/funnelchat-grupos",
"description": "Entradas y salidas de grupos",
"events": ["group_participant.joined", "group_participant.left"],
"filters": {},
"status": "active",
"disabled_reason": null,
"last_delivery_at": null,
"last_success_at": null,
"secret_rotated_at": null,
"created_at": "2026-10-01T12:00:00+00:00",
"updated_at": "2026-10-01T12:00:00+00:00",
"migration": null,
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"secret_notice": "Guardá este secreto ahora: es la única vez que se muestra. Con él validás la firma del header de cada entrega."
}El secreto se ve una sola vez
Guardá secret completo, con el prefijo whsec_: es parte del secreto y la firma se calcula con él tal cual. No se puede volver a consultar; si lo perdés, rotalo desde el panel de Funnelchat. Guardá también id para identificar tu suscripción (detalle).
2. Probá la suscripción
Enviar un evento de prueba manda ya un webhook.test a tu URL y te dice cómo le fue. {id} es el id del paso anterior (en el ejemplo de curl, la variable WEBHOOK_ID). Pide webhooks:write.
curl -X POST "https://api.funnelchat.com/api/v1/webhooks/$WEBHOOK_ID/test" \
-H "Authorization: Bearer $FC_API_KEY" \
-H "Accept: application/json"Respuesta de ejemplo (200, con los campos de la especificación):
{
"succeeded": true,
"response_status": 200,
"error": null,
"duration_ms": 1354,
"delivery_id": 9013
}Si succeeded es false, response_status y error dicen por qué (non_2xx, timeout, connection_failed, tls_error, destination_not_allowed…), explicado en Webhooks. La prueba no se reintenta.
3. Procesá cada entrada y salida
Cuando alguien entra a un grupo de una de tus campañas, llega esto (ejemplo del catálogo de eventos); la salida, group_participant.left, trae los mismos campos:
{
"id": "evt_00000000000000000000000000000002",
"type": "group_participant.joined",
"version": 1,
"occurred_at": "2026-10-01T12:00:00Z",
"data": {
"group": {
"uid": "120363000000000001@g.us",
"name": "Grupo de ejemplo"
},
"campaign": {
"uuid": "00000000-0000-4000-8000-000000000001"
},
"participant": {
"identity": "+5491100000000",
"identity_type": "phone",
"name": "Ana Pérez"
}
}
}| Campo | Qué es |
|---|---|
data.group.uid | Identifica al grupo. Las lecturas de los pasos 4 y 5 también lo identifican por su uid. |
data.campaign.uuid | La campaña a la que pertenece el grupo. |
data.participant.identity | Quién entró o salió. Con identity_type phone, es un teléfono con + y código de país. |
data.participant.identity_type | Hoy, phone. Pueden aparecer otros valores sin aviso (identity_type). |
occurred_at | Cuándo pasó, en UTC. |
Después de verificar la firma y responder 2xx (paso 1 de Recibir los mensajes), procesá el evento así:
// miembros: en producción, una tabla con (grupo, persona) → { activo, ocurrioEn }
const miembros = new Map();
const procesados = new Set();
function procesarEventoDeGrupo(evento) {
if (procesados.has(evento.id)) return; // repetido
if (evento.type !== 'group_participant.joined' && evento.type !== 'group_participant.left') return;
const { group, participant } = evento.data;
if (participant.identity_type !== 'phone') return; // valor nuevo: ignoralo (ya respondiste 2xx)
const clave = `${group.uid}|${participant.identity}`;
const anterior = miembros.get(clave);
// Un reintento puede llegar después de un evento posterior: no pises lo más nuevo.
if (!anterior || new Date(evento.occurred_at) > new Date(anterior.ocurrioEn)) {
miembros.set(clave, {
activo: evento.type === 'group_participant.joined',
nombre: participant.name,
ocurrioEn: evento.occurred_at,
});
}
procesados.add(evento.id);
}- Los eventos son de grupos de campañas. El catálogo los describe así: «Una persona entró a un grupo de una campaña». Los participantes de cualquier grupo se leen igual con el paso 4.
- Compará fechas como fechas. communities escribe las fechas con
Zy conversations con+00:00: usá un parser ISO 8601, no compares texto.
4. Cargá los participantes actuales
Para la carga inicial, o para reconciliar de vez en cuando, Listar los participantes de un grupo devuelve quién está hoy en un grupo. groupUid (obligatorio) es el uid del grupo; si todavía no los tenés, sacalos de Listar grupos. Con una clave, la respuesta siempre viene paginada: perPage de 1 a 100 (25 si no lo mandás).
curl -G "https://api.funnelchat.com/api/v1/groups/participants" \
--data-urlencode "groupUid=120363000000000001@g.us" \
--data-urlencode "perPage=100" \
--data-urlencode "orderBy=id" \
--data-urlencode "orderDirection=asc" \
-H "Authorization: Bearer $FC_API_KEY" \
-H "Accept: application/json"Respuesta de ejemplo (recortada a un participante y sin links; … marca valores cuyo formato la especificación no documenta):
{
"data": [
{
"id": "…",
"phone": "…",
"action": "…",
"isAdmin": "…",
"created_at": "…"
}
],
"meta": {
"current_page": 1,
"from": 1,
"last_page": 3,
"path": "https://api.funnelchat.com/api/v1/groups/participants",
"per_page": 100,
"to": 100,
"total": 248
}
}Pedí la página siguiente con page, el mismo perPage, el mismo orden y el mismo groupUid, y pará cuando meta.current_page llegue a meta.last_page. Para filtrar, el listado acepta también phone, isAdmin y eligibleForAdmin.
5. Recuperá lo que te perdiste
Si tu receptor estuvo caído más de lo que cubren los reintentos (4 intentos por evento), Listar entradas y salidas de un grupo te da el historial de un grupo: quién entró y quién salió, y cuándo. {group} es el uid del grupo. Filtrá desde la última entrega que procesaste con start_date (y end_date), y por tipo con event_type (all, join o leave).
curl -G "https://api.funnelchat.com/api/v1/groups/120363000000000001@g.us/participant-events" \
--data-urlencode "start_date=2026-10-01T00:00:00Z" \
--data-urlencode "event_type=all" \
--data-urlencode "perPage=100" \
--data-urlencode "page=1" \
-H "Authorization: Bearer $FC_API_KEY" \
-H "Accept: application/json"Respuesta de ejemplo (recortada a un movimiento y sin links ni meta; … marca valores cuyo formato la especificación no documenta):
{
"data": [
{
"phone": "…",
"whatsapp_id": "…",
"event_type": "join",
"event_at": "…",
"group_id": 1234,
"group_name": "Grupo de ejemplo",
"country_code": "…",
"country_name": "…"
}
]
}event_type es join o leave. Para la campaña entera, Listar entradas y salidas de una campaña acepta los mismos filtros, pero pide el ID numérico de la campaña, no el uuid que traen los eventos.
- El formato de
phoneen las lecturas de communities. Los eventos traen el teléfono con+; para cruzarlos con estas listas, compará solo los dígitos. - Qué valores toman
isAdminyactionen los participantes: la especificación los declara como texto, sin la lista de valores. - El formato de
event_aten el historial, y sigroup_id(un entero) tiene relación con eluiddel grupo. Identificá el grupo por eluidque usaste en la ruta. pageen Participantes de un grupo. El listado pagina y sumetatraecurrent_pageylast_page, peropageno figura entre sus parámetros. Comprobá con tu clave que la segunda página trae participantes distintos de la primera.
Si falla
| Qué ves | Qué pasó | Qué hacer |
|---|---|---|
401 | Falta la clave, no existe o fue revocada. El cuerpo trae solo message («Unauthenticated.»). | Revisá la cabecera Authorization: Bearer con la clave completa. |
403 con error: "permission_denied" | A la clave le falta un permiso: webhooks:write para crear y probar la suscripción, webhooks:read para verla, o communities:read para leer grupos y para los eventos de grupos de la suscripción. | Creá una clave con ese permiso. message dice cuál falta. |
403 con error: "route_not_available" | Intentaste una operación de communities que no está abierta a claves: una clave solo lee en communities. | No hay permiso que pedir. Las rutas que no figuran en la referencia no están disponibles para claves. |
422 al crear | La validación falló: un tipo de events que no existe, una url inválida, o a la clave le falta communities:read para los eventos de grupos. | errors dice qué corregir. |
422 al probar | La suscripción está disabled: no se envía nada. | Mirá disabled_reason. Con key_revoked, creá una nueva; si se deshabilitó por fallas, arreglá el receptor y reactivala con PUT y status: "active". |
422 en las lecturas | perPage fuera de 1 a 100 o un orden no admitido. | errors dice qué corregir. |
404 | No existe el grupo o la suscripción que pediste. | Revisá el uid del grupo o el id de la suscripción. |
429 | La clave superó su tope diario en communities (daily_cap_exceeded, con Retry-After). | Esperá lo que indica Retry-After y reintentá. |
503 con error: "gate_unavailable" | El portero no pudo evaluar la llamada. | Esperá lo que indica Retry-After y reintentá. |
El detalle de los códigos está en Errores.
Relacionado
- Webhooks: la firma, los reintentos y el historial de entregas.
- Catálogo de eventos: los demás eventos de grupos, como
group.blockedogroup.invite_link_down. - Recibir los mensajes de WhatsApp en tu sistema: el receptor paso a paso.
- Listar entregas de un webhook: qué intentos se hicieron y con qué resultado.
- Listar grupos: tus grupos, con filtros por estado, número o campaña.