Avisos a una lista
Cuando tu sistema tiene que avisarle a muchas personas a la vez (los vecinos de una torre, el personal de turno, los alumnos de un curso), la lista la mantiene una persona de tu equipo en el panel de Alttos: etiqueta a los contactos y tu software los recorre con GET /v1/contacts?tag=, sin llevar la lista de teléfonos adentro.
Es una etiqueta de tu CRM, no un grupo 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.
El nombre de la etiqueta es lo que pides en ?tag=: si alguien la renombra en el panel, actualiza el nombre en tu configuración.
Enviar el aviso, uno por uno
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 contactos de la etiqueta con GET /v1/contacts?tag=, 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']}"}
ETIQUETA = "vecinos-torre-a" # la etiqueta que tu equipo mantiene en el panel
CHANNEL = "1b1a6d68-6e78-4717-aa04-5ace8614e907"
EVENTO = "corte-agua-2026-09-14" # identificador del aviso en TU sistema
def contactos(etiqueta):
cursor = None
while True:
params = {"tag": etiqueta, "limit": 100, **({"cursor": cursor} if cursor else {})}
page = requests.get(f"{API}/contacts", 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 contactos(ETIQUETA):
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 una lista 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 lista 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 una lista 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 la lista 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.