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:read y webhooks:write. communities:read lee los grupos y sus participantes, y es además el permiso del área de los eventos de grupos, que la suscripción pide; webhooks:write crea 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 https pú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 a message.received, podés sumarle estos eventos con PUT en 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"
    }
  }
}
CampoQué es
data.group.uidIdentifica al grupo. Las lecturas de los pasos 4 y 5 también lo identifican por su uid.
data.campaign.uuidLa campaña a la que pertenece el grupo.
data.participant.identityQuién entró o salió. Con identity_type phone, es un teléfono con + y código de país.
data.participant.identity_typeHoy, phone. Pueden aparecer otros valores sin aviso (identity_type).
occurred_atCuá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 Z y 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.

Si falla

Qué vesQué pasóQué hacer
401Falta 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 crearLa 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 probarLa 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 lecturasperPage fuera de 1 a 100 o un orden no admitido.errors dice qué corregir.
404No existe el grupo o la suscripción que pediste.Revisá el uid del grupo o el id de la suscripción.
429La 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

En esta página