POST /api/v1/campaigns/{campaignId}/send
Lanza el envío y responde 202 de inmediato. El envío en sí se ejecuta en segundo plano, al ritmo que permite la cuota de envío: la solicitud nunca lo espera.
Llamado sin cuerpo, envía ahora. Llamado con un scheduledAt, reserva el envío para ese instante: mismo endpoint, misma clave, mismos permisos.
json
{"success": true, "data": {"campaignId": "…", "status": "send_requested"}}Programación: un campo opcional, el mismo endpoint
Sin cuerpo, con un cuerpo vacío o con un cuerpo sinscheduledAt, todo significa lo mismo: enviar ahora. Las llamadas escritas antes de que existiera este campo no tienen nada que cambiar.
bash
curl -X POST -H "x-api-key: $AGENTMAIL_API_KEY" -H "content-type: application/json" \
-d '{"scheduledAt": "2026-08-12T07:00:00Z"}' \
"https://www.agentsmail.io/api/v1/campaigns/$CAMPAIGN_ID/send"json
{"success": true, "data": {"campaignId": "…", "status": "scheduled"}}status distingue los dos casos: send_requested para un envío que empieza ahora, scheduled para uno reservado para más tarde. La campaña pasa a scheduled, y GET /api/v1/campaigns/{campaignId} devuelve el instante realmente guardado; vale la pena leerlo una vez, porque la programación se guarda al minuto y los segundos que envíes se descartan.
Una fecha sin zona horaria se rechaza; nunca se interpreta como UTC.
2026-08-12T09:00:00 responde 400: la API no adivina un desfase que no escribiste, porque adivinarlo mal envía con una o dos horas de diferencia sin que nada en la solicitud lo muestre. Escribe 2026-08-12T07:00:00Z o 2026-08-12T09:00:00+02:00: ambos designan el mismo instante. Un instante en el pasado, o a menos de un par de minutos, se rechaza de la misma forma.La programación es para un
draft, y pausar/reanudar se hace en la app. Una campaña que ya está scheduled responde 400: no se reprograma sobre la marcha. Pausar o reanudar una programación se hace desde la app, en la pantalla de la campaña; ningún endpoint lo expone. Reserva la hora que realmente quieres.| Estado de la campaña | POST /send | con scheduledAt |
|---|---|---|
draft | 202: se solicita el envío | 202: la campaña queda programada |
scheduled | 202: sale ya | 400: solo un borrador se puede programar |
paused | 409: reanúdala en la app | 409 |
sending | 409: ya hay un envío en curso | 409 |
sent | 409: ya salió | 409 |
failed | 409: no está en un estado enviable | 409 |
| sin enlace de baja en el contenido | 400: no se emite nada | 400: no se programa nada |
| desconocida, o de otra organización | 404 | 404 |
El contenido se renderiza antes de emitir nada. Es exactamente la misma comprobación que
POST /api/v1/render: una campaña cuyo HTML no lleva {{unsubscribeUrl}} responde 400 y no se crea ningún envío. Sin él, cada envío fallaría más tarde por un motivo que la API nunca te muestra. Corrige la plantilla, vuelve a crear la campaña y envía.Llamar dos veces a send nunca envía dos veces. Una campaña que ya salió responde
409 y no se inicia un segundo envío: reintentar tras un timeout es seguro. Si quieres enviar el mismo contenido otra vez, crea una campaña nueva.GET /api/v1/campaigns/{campaignId}: status pasa de draft → sending → sent, y progress se va llenando por el camino. No hay ningún webhook que esperar.
Autorización
| Rol en la organización | Este endpoint |
|---|---|
OWNER | Permitido |
ADMIN | Permitido |
MEMBER | Rechazado — 403 |
Cuerpo de la solicitud
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
scheduledAt | string | no | Instante ISO 8601 con un desfase explícito. Si se omite, la campaña sale ya |
Ejemplo
bash
curl -X POST -H "x-api-key: $AGENTMAIL_API_KEY" \
"https://www.agentsmail.io/api/v1/campaigns/$CAMPAIGN_ID/send"Respuesta
202 Accepted: el envío está en cola; síguelo en la campaña.401 Unauthorized: clave ausente o no válida, o su propietario dejó la organización.403 Forbidden: el propietario de la clave esmemberde la organización, noadmin.404 Not Found: no existe ese recurso, o pertenece a otra organización. La API nunca confirma que un id existe a quien no tiene derecho sobre él.429 Too Many Requests: más de 120 solicitudes en un minuto para esta clave.
Campos de la respuesta
| Campo | Tipo | Descripción |
|---|---|---|
success | boolean | Indica si la operación tuvo éxito |
data.status | string | send_requested cuando sale ya, scheduled cuando se indicó una fecha |
Ejemplo de respuesta
json
{
"success": true,
"data": {
"status": "subscribed"
}
}Receta para agentes
Un envío es asíncrono: la llamada vuelve en cuanto se acepta la salida, no cuando los correos han salido. Sondea la campaña para seguirlo, y deja de sondear cuando el envío está desactivado, o el bucle nunca converge.js
await fetch(`${base}/campaigns/${campaignId}/send`, {method: 'POST', headers})
for (;;) {
const {data} = await (
await fetch(`${base}/campaigns/${campaignId}`, {headers})
).json()
if (!data.sendingEnabled) throw new Error('sending is paused: polling would never end')
if (data.status === 'sent' || data.status === 'failed') return data
await new Promise((resolve) => setTimeout(resolve, 5000))
}Nunca llames a esto sin la aprobación humana explícita para esa campaña concreta. La aprobación de un envío nunca cubre el siguiente.