Grupos
Un grupo es una lista de contactos que tu equipo arma y mantiene desde el panel: los vecinos de una torre, los alumnos de un curso, el personal de turno. La API te deja leer esos grupos y recorrer sus integrantes.
Para qué sirve: tu sistema manda un aviso a todos los integrantes de un grupo sin tener la lista de teléfonos adentro. Quien administra la lista es una persona en el panel de Alttos, que agrega y quita gente sin tocar tu código.
Estos grupos son listas de contactos de tu CRM, no grupos de WhatsApp. El aviso le llega a cada persona como un chat individual, así que nadie ve el teléfono de los demás y cada respuesta entra como una conversación propia que tu agente o tu equipo pueden atender.
Listar los grupos
Necesitas una API key con el permiso contacts:read.
curl https://api.alttos.ai/api/public/v1/groups \
-H "Authorization: Bearer $ALTTOS_API_KEY"{
"data": [
{
"id": "9f0d2f4c-4f1a-4a5e-8c7d-2b9a1f0e3d55",
"name": "Torre A",
"description": "Propietarios y residentes de la torre A",
"member_count": 48,
"created_at": "2026-09-04T23:20:30.196822Z"
}
],
"next_cursor": null
}| Campo | Qué es |
|---|---|
id | Lo que va en la URL para pedir los integrantes. Es lo único estable: el nombre lo puede cambiar cualquiera desde el panel, así que guarda el id en tu configuración, no el nombre. |
name | Como se llama el grupo en el panel. |
description | La descripción que le pusieron, o null. |
member_count | Cuántos contactos tiene ahora mismo. Es el número de integrantes que vas a recorrer. |
created_at | Cuándo se creó. |
Los grupos vienen ordenados por nombre. La lista pagina por cursor, igual que el resto de los listados: limit entre 1 y 100 (50 por defecto) y cursor para la página siguiente.
Listar los integrantes
curl https://api.alttos.ai/api/public/v1/groups/9f0d2f4c-4f1a-4a5e-8c7d-2b9a1f0e3d55/members \
-H "Authorization: Bearer $ALTTOS_API_KEY"{
"data": [
{
"id": "c16c6b03-3990-4097-9819-5f80ca66fefb",
"name": "Ana Restrepo",
"phone": "+573001112233",
"email": "ana@ejemplo.com",
"tags": ["torre-a"],
"created_at": "2026-09-04T23:20:30.196822Z",
"last_activity_at": "2026-09-05T14:02:11.004411Z"
}
],
"next_cursor": null
}Cada integrante viene con el mismo contenido que GET /v1/contacts, phone incluido: eso es lo que te permite enviarle sin pedir cada contacto por separado.
Un group_id que no existe en tu cuenta responde 404 group_not_found.
Un contacto puede no tener teléfono (por ejemplo, si entró por otro canal). Filtra por phone no nulo antes de enviar, o tu ciclo va a acumular errores evitables.
Enviar un aviso a todo el grupo
No hay un endpoint que envíe a todos de una vez, y es a propósito: cada persona recibe un mensaje individual, con su propia conversación, su propio estado de entrega y su propio reintento. Así, cuando alguien responde, respondes a esa persona y no a una lista.
El ciclo es: pedir los integrantes, y enviar uno por uno con POST /v1/messages.
import os, time, uuid, requests
API = "https://api.alttos.ai/api/public/v1"
HEAD = {"Authorization": f"Bearer {os.environ['ALTTOS_API_KEY']}"}
GROUP = "9f0d2f4c-4f1a-4a5e-8c7d-2b9a1f0e3d55"
CHANNEL = "1b1a6d68-6e78-4717-aa04-5ace8614e907"
EVENTO = "corte-agua-2026-09-14" # identificador del aviso en TU sistema
def integrantes(group_id):
cursor = None
while True:
params = {"limit": 100, **({"cursor": cursor} if cursor else {})}
page = requests.get(f"{API}/groups/{group_id}/members", headers=HEAD, params=params).json()
yield from page["data"]
cursor = page["next_cursor"]
if not cursor:
return
def enviar(persona):
"""Devuelve la respuesta. Reintenta solo los 429 que se resuelven esperando."""
for intento in range(5):
r = requests.post(
f"{API}/messages",
headers={**HEAD, "Idempotency-Key": f"{EVENTO}:{persona['phone']}"},
json={
"channel_id": CHANNEL,
"to": {"phone": persona["phone"]},
"content": {
"type": "template",
"name": "aviso_mantenimiento",
"language": "es",
"variables": {"1": persona["name"] or "vecino", "2": "sábado de 8 a 12"},
},
},
)
if r.status_code != 429:
return r
if r.json()["error"]["code"] == "tier_exhausted":
raise CupoAgotado(r.json()["error"]["detail"]) # esperar horas, no segundos
# Tope por minuto: Retry-After viene, pero léelo a la defensiva.
time.sleep(int(r.headers.get("Retry-After", 2 ** intento)))
return r
class CupoAgotado(Exception):
pass
pendientes = []
for persona in integrantes(GROUP):
if not persona["phone"]:
continue
try:
enviar(persona)
except CupoAgotado:
pendientes.append(persona) # reanuda mañana: misma Idempotency-Key, no duplica
breakFíjate en que no todos los 429 se tratan igual. El del tope por minuto se resuelve esperando segundos y trae Retry-After. El de tier_exhausted no trae Retry-After y no se resuelve esperando un rato: hay que parar y retomar más tarde. Un bucle que reintente los dos igual se queda martillando la API sin avanzar. Lo explicamos abajo.
Hay dos cosas de ese ejemplo que no son opcionales.
La Idempotency-Key va atada al aviso, no al momento
f"{EVENTO}:{persona['phone']}" combina el identificador del aviso en tu sistema con el teléfono. Si tu proceso se cae a mitad de camino y lo vuelves a lanzar, quienes ya recibieron el mensaje no lo reciben de nuevo: reconocemos la clave y te devolvemos el envío original.
Si en cambio usaras algo distinto en cada intento (un uuid4(), la hora actual), cada corrida sería un envío nuevo y la gente recibiría el mismo aviso dos veces. Es el error más caro de esta receta, porque el duplicado le llega a personas reales.
Hay dos 429 distintos y no se reintentan igual
El tope por minuto (rate_limited, channel_throttled) es el esperable: puedes enviar hasta 120 mensajes por minuto por cuenta y 60 por minuto por cada canal de WhatsApp, así que un grupo de 150 personas lo va a tocar. No es un error de tu integración. Trae Retry-After, esperas esos segundos y continúas.
El cupo diario del número (tier_exhausted) es otra cosa: no trae Retry-After y no se arregla esperando un rato. Ahí hay que cortar el recorrido y retomarlo más tarde, guardando a quién te faltó. Como la Idempotency-Key va atada al aviso y no al intento, retomar mañana no le manda el mensaje dos veces a nadie.
Y hay un tercero que viene de WhatsApp, provider_rate_limited, que tampoco trae Retry-After: para ese, espera creciente. Todos los detalles están en Errores y límites.
El límite que de verdad importa: cuántas personas nuevas puedes alcanzar hoy
Además de nuestros límites por minuto, WhatsApp le pone a tu número un cupo de personas nuevas que puedes contactar cada 24 horas. Es el límite que más sorprende, porque no depende de tu plan con nosotros sino de la madurez de tu número en Meta.
Tres cosas que conviene entender antes de mandar tu primera alerta:
- Se cuentan personas, no mensajes. Tres plantillas al mismo número en el día consumen un destinatario, no tres. Y responder dentro de la ventana de 24 horas no consume nada.
- Es una ventana móvil, no un contador que se reinicia a medianoche: el cupo se va liberando hora a hora, a medida que los envíos viejos salen de las últimas 24 horas.
- El cupo sube solo. Un número nuevo arranca bajo (a veces en 50 personas por día) y Meta lo va subiendo a 250, 1.000, 10.000 y más a medida que envías con buena calidad y la gente no te bloquea. No hay que pedirlo.
Consúltalo antes de disparar
curl https://api.alttos.ai/api/public/v1/channels/{channel_id}/capacity \
-H "Authorization: Bearer $ALTTOS_API_KEY"{
"tier": "TIER_1K",
"quality_rating": "GREEN",
"capacity": 1000,
"used_24h": 148,
"remaining": 852,
"synced_at": "2026-09-12T14:02:11Z"
}remaining es cuántas personas nuevas puedes alcanzar ahora mismo. Si tu grupo es más grande que ese número, parte el envío en tandas o espera: no hay forma de acelerarlo.
capacity y remaining vienen en null cuando tu número no tiene techo, y en ese caso no hay nada que calcular. quality_rating en YELLOW o RED es la señal de alarma temprana: una calidad degradada es lo que precede a que Meta te baje el cupo.
Si se agota, POST /v1/messages responde 429 tier_exhausted antes de gastar el envío, con capacity y used_24h en detail. Reintenta más tarde con la misma Idempotency-Key: ese 429 la libera, así que el reintento se ejecuta de verdad. A quien ya contactaste dentro de la ventana le puedes seguir escribiendo aunque el cupo esté lleno.
Cuándo usar una plantilla
Si la persona no te escribió en las últimas 24 horas, WhatsApp solo permite mensajes con plantilla aprobada. Un aviso a un grupo casi siempre cae en ese caso, así que el ejemplo usa content.type: "template".
Elige la categoría correcta al crear la plantilla en Meta: un corte de agua o un recordatorio es UTILITY; una promoción es MARKETING. No es un detalle de forma. Las de marketing respetan el apagado de promociones del destinatario (422 recipient_opted_out), que es exactamente lo que quieres para una oferta y exactamente lo que no quieres para un aviso de servicio. Puedes ver las plantillas disponibles y sus variables con GET /v1/channels/{id}/templates.
Cuando el grupo es grande
Esta receta está pensada para avisos de decenas de personas. Si mandas a cientos o miles, o quieres ver el resultado agregado del envío (cuántos llegaron, cuántos fallaron, pausar a mitad), usa Campañas desde el panel: ahí el envío corre de nuestro lado, con su estimación de costo antes de lanzar, su tablero de entrega y su reanudación automática si WhatsApp limita tu número.