Campañas
Correos de marketing para un segmento de la audiencia (endpoint /broadcasts), con personalización, prueba, programación, baja y estadísticas.
Las campañas (endpoint /broadcasts) son envíos de marketing a un segmento de tu audiencia, opcionalmente solo a quienes están suscritos a un tema. Escribe el contenido en la propia campaña (html y text) o usa una plantilla publicada, personaliza con los datos de cada contacto, envía una prueba y envía al instante o programa. Cada destinatario recibe su propio correo electrónico, con un enlace para cancelar la suscripción y las cabeceras List-Unsubscribe. Todos los endpoints exigen una clave de acceso total.
#Cómo funciona
- Crea la campaña como borrador (
POST /broadcasts). Solo el nombre es obligatorio. - Comprueba a cuántos contactos llega (
GET /broadcasts/audience), mira la vista previa personalizada y envía una prueba. - Envía o programa (
POST /broadcasts/{id}/send). En ese momento Postal exige el segmento, un remitente de un dominio verificado, el asunto y el contenido. - Haz el seguimiento del progreso y las estadísticas en la propia campaña, en destinatarios y en las métricas.
Quién recibe: los contactos del segmento que no se han dado de baja (unsubscribed), no están en la lista de supresión y, si hay topic_id, están suscritos al tema (eligieron recibir o, en un tema opt_in, no eligieron nada). La lista se lee por lotes durante el envío, así que quien se da de baja a mitad del proceso no recibe.
En un segmento dinámico, los filtros se evalúan en el momento del envío: recibe quien los cumple en ese instante.
Cada destinatario se convierte en un correo electrónico normal: cuenta en la cuota de envío, aparece en GET /emails y genera los webhooks email.* con la etiqueta broadcast_id. Si la cuota se agota a mitad de camino, la campaña queda paused y continúa sola cuando la cuota se renueve. Ningún contacto recibe la misma campaña dos veces.
Estado
| Estado | Qué significa | Qué se puede hacer |
|---|---|---|
draft | Borrador. Puede quedar incompleta; lo que el envío exige se comprueba al enviar. | Editar, eliminar, enviar, programar |
scheduled | Programada para scheduled_at. Aún se puede editar, pero debe seguir estando lista para el envío. La zona horaria en la que elegiste la hora queda en schedule_timezone, solo para mostrarla: el envío sale en scheduled_at. | Editar, eliminar, reprogramar, enviar ya, cancelar |
queued | En cola para empezar el envío. | Cancelar |
sending | Enviando, en lotes de hasta 500 contactos. processed_count muestra el progreso. | Cancelar |
paused | Detenida a mitad del envío, con el motivo en error. Con error.resumes_at (la cuota se agotó), se reanuda sola cuando la cuota se renueva; sin él (envíos del equipo en pausa), se reanuda cuando vuelvas a enviar. El error.code es daily_quota_exceeded o monthly_quota_exceeded (la cuota de envío se agotó; scheduled_at muestra cuándo se renueva; las campañas no usan la reserva del Adicional de envíos) o sending_paused (los envíos del equipo se pausaron por la reputación; el webhook sending.paused avisa, y el envío solo se acepta de nuevo después de sending.resumed). | Volver a enviar, cancelar |
sent | Todos los destinatarios se procesaron. Dispara el webhook broadcast.sent. | Duplicar |
canceled | Cancelada. Quien aún no había recibido ya no recibe. | Duplicar |
failed | No se pudo enviar (por ejemplo, la plantilla dejó de estar publicada o el dominio perdió la verificación). error.code: invalid_broadcast o send_failed. | Duplicar |
#Personalización
En el asunto, en el HTML y en el texto, usa variables entre triples llaves. Después de |, un valor predeterminado para cuando el contacto no tiene el dato:
{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você
| Variable | Valor |
|---|---|
{{{FIRST_NAME}}}, {{{LAST_NAME}}}, {{{EMAIL}}} | Nombre, apellido y correo electrónico del contacto. También como {{{contact.first_name}}}, {{{contact.last_name}}} y {{{contact.email}}}. |
{{{contact.plan}}} | Cualquier propiedad del contacto, por su clave. Si el contacto no tiene valor, usa el predeterminado en línea o el fallback_value de la propiedad. |
{{{COUPON}}} | Variables de la plantilla (con template_id), rellenadas con variables o con el valor predeterminado declarado en la plantilla. |
{{{POSTAL_UNSUBSCRIBE_URL}}} | El enlace a la página de preferencias y baja del contacto. Consulta Cancelación de suscripción. |
El orden es: el valor del contacto, el predeterminado después de |, el fallback_value de la propiedad y, por último, vacío. En el HTML, los valores se escapan. El preheader entra como texto oculto al comienzo del cuerpo, el fragmento que los buzones de entrada muestran junto al asunto. Sin text, la versión en texto se genera a partir del HTML.
Con template_id, el contenido viene de la versión publicada de la plantilla, y el subject de la campaña, si existe, vale en lugar del asunto de la plantilla. Las variables declaradas en la plantilla sin valor y sin predeterminado impiden el envío (422); FIRST_NAME, LAST_NAME, EMAIL y POSTAL_UNSUBSCRIBE_URL las rellena el contacto.
#Cancelación de suscripción
Todo correo de campaña lleva un enlace firmado para cancelar la suscripción, solo de ese contacto, a la página de preferencias en https://forms.postal.so/preferencias/…. Allí, la persona sale de todos los envíos, vuelve a recibirlos o elige los temas públicos.
- Pon el enlace donde quieras con
{{{POSTAL_UNSUBSCRIBE_URL}}}. Si el HTML no tiene la variable, Postal añade al final un pie discreto: “Não quer mais receber estes e-mails? Descadastre-se.” (en inglés: “Do not want to receive these emails anymore? Unsubscribe.”). La versión en texto recibe una línea con el enlace (“Para não receber mais estes e-mails: …” o “To stop receiving these emails: …”). El idioma del pie sigue las reglas de Idioma del pie y de la página. - Las cabeceras
List-UnsubscribeyList-Unsubscribe-Post: List-Unsubscribe=One-Clickvan en todos los correos, para el botón “Cancelar suscripción” de los buzones de entrada. Al hacer clic, si la campaña tiene tema, el contacto sale solo del tema; si no, se da de baja de todo. - Cada baja cuenta una vez en
stats.unsubscribedde la campaña y disparacontact.updated.
Idioma del pie y de la página
El pie automático y la página de preferencias (con los avisos que muestra) hablan el idioma de quien recibe, pt-BR o en. Postal lo elige para cada destinatario, usando el primero que exista:
- el
languagede la campaña (pt-BRoen); - la propiedad
languagedel contacto, comopt-BRoen-US(vale el idioma anterior al guion; los demás idiomas se ignoran); - la propiedad
localedel contacto, de la misma forma; - el idioma que el propietario del equipo eligió en el panel;
- portugués de Brasil, cuando nada indica cuál es el idioma.
La página de preferencias usa el mismo orden. La campaña que vale es la del correo en el que la persona hizo clic (el enlace lleva el ID del correo); los enlaces de automatizaciones y los enlaces antiguos, sin el correo de la campaña, empiezan por el contacto. El Accept-Language del navegador no cuenta en estas páginas. La página de enlace no válido (enlace alterado, o de un contacto que no existe) elige así: el idioma del contacto citado en el enlace (el de las propiedades o, si no lo tiene, el del equipo), después el Accept-Language del navegador (pt-BR o en) y, por último, portugués de Brasil.
Para fijar el idioma de una campaña, envía language al crearla o actualizarla. Postal no traduce tu contenido (asunto, HTML, texto): solo lo que ella misma añade.
Detalles y buenas prácticas en la guía Marketing por correo electrónico.
#Detectar respuestas
Con detect_replies: true, cada correo de la campaña sale con un Reply-To propio, con el formato r-<token>@…, que vale solo para ese correo: en el dominio del remitente, si recibe correos por Postal, o en el dominio de recepción de Postal. Cuando la persona responde, Postal registra la respuesta en el correo y la reenvía a tu reply_to (o, si no lo tienes, al remitente from), así que sigues recibiendo las respuestas con normalidad.
- La respuesta dispara el webhook
email.replied, conreply.from,reply.subject,reply.previewyreply.auto. - Las respuestas automáticas (fuera de la oficina, autorrespondedores, avisos de entrega) llegan con
reply.auto: truey no cuentan como respuesta. - Cada correo electrónico reenvía como máximo 20 respuestas, para que dos autorrespondedores no se queden intercambiando mensajes para siempre.
Las automatizaciones tienen la misma opción en el paso send_email, y el paso wait_for puede esperar la respuesta.
#Listar campañas
GET/broadcasts
De la más reciente a la más antigua. La lista trae el resumen, sin html, text, variables ni editor.
Parámetros de consulta
limitintegerDe 1 a 100. Predeterminado: 20.afterstringCursor de la página siguiente.beforestringCursor de la página anterior.searchstringBusca en el nombre y en el asunto.statusstringUno o más estados separados por comas, comoscheduled,sending.
cURL
curl 'https://api.postal.so/broadcasts?status=sent' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts?status=sent', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/broadcasts",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"status": "sent",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('broadcasts', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'status' => 'sent',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "list",
"has_more": false,
"data": [
{
"object": "broadcast",
"id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"name": "Black Friday: acesso antecipado",
"status": "sent",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"segment": {
"id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"name": "Clientes ativos"
},
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"topic": {
"id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"name": "Novidades"
},
"template_id": null,
"from": "Loja Exemplo <[email protected]>",
"reply_to": ["[email protected]"],
"detect_replies": false,
"subject": "{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você",
"preheader": "Até 40% de desconto a partir de quinta, só para clientes.",
"language": null,
"scheduled_at": null,
"schedule_timezone": null,
"queued_at": "2026-10-22T12:00:00.000000Z",
"sent_at": "2026-10-22T12:04:31.000000Z",
"canceled_at": null,
"recipients_count": 1247,
"processed_count": 1247,
"stats": {
"sent": 1247,
"delivered": 1236,
"opened": 612,
"clicked": 188,
"bounced": 6,
"complained": 1,
"unsubscribed": 9,
"failed": 0
},
"error": null,
"created_at": "2026-10-21T18:40:00.000000Z",
"updated_at": "2026-10-22T12:04:31.000000Z"
}
]
}
#Crear una campaña
POST/broadcasts
Crea un borrador (draft). Puede faltar todo menos el nombre: lo que exige el envío se comprueba en el envío. El segmento, el tema y la plantilla deben ser de tu equipo.
Cuerpo de la solicitud
namestringobligatorioNombre interno, hasta 255 caracteres. Obligatorio al crear.segment_idstring | nullSegmento que recibe la campaña. Obligatorio para enviar.topic_idstring | nullTema: solo recibe quien está suscrito a él, y el clic en “Cancelar suscripción” quita al contacto solo de este tema.fromstring | nullRemitente, comoLoja Exemplo <[email protected]>, de un dominio verificado del equipo. Obligatorio para enviar.reply_tostring | string[] | nullDirecciones de respuesta.detect_repliesbooleanPor defecto:false. Contrue, cada correo electrónico lleva una dirección de respuesta propia y Postal registra y reenvía las respuestas. Consulta Detectar respuestas.subjectstring | nullAsunto, hasta 998 caracteres, con variables. Obligatorio para enviar, a menos que la plantilla tenga asunto.preheaderstring | nullTexto de vista previa, hasta 500 caracteres.languagestring | nullIdioma de lo que Postal escribe en los correos electrónicos de esta campaña:pt-BRoen. Vale para el pie de baja automático y para la página de preferencias que abre el enlace;null(por defecto) deja que Postal elija por destinatario. Consulta Baja. No traduce tu contenido.htmlstring | nullCuerpo en HTML, hasta 2 MB. Para enviar, hace faltahtml,textotemplate_id.textstring | nullVersión en texto, hasta 2 MB.template_idstring | nullvariablesobject | nullValores de las variables de la plantilla, como{"COUPON": "BF40"}.editorobject | nullEstado del editor visual del panel. Guárdalo y devuélvelo tal como llegó.
Errores
- 422
validation_errorCampo no válido, como una dirección fuera del formatoNome <[email protected]>o un ID que no es UUID. - 404
not_foundEl segmento, el tema o la plantilla no existe.
cURL
curl -X POST https://api.postal.so/broadcasts \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Black Friday: acesso antecipado",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"from": "Loja Exemplo <[email protected]>",
"reply_to": "[email protected]",
"subject": "{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você",
"preheader": "Até 40% de desconto a partir de quinta, só para clientes.",
"language": "pt-BR",
"html": "<p>Oi, {{{FIRST_NAME|cliente}}}!</p><p>Separamos descontos de até 40% para quem é do plano {{{contact.plan}}}.</p><p><a href=\"https://lojaexemplo.com.br/black-friday\">Ver as ofertas</a></p><p><a href=\"{{{POSTAL_UNSUBSCRIBE_URL}}}\">Não quero mais receber</a></p>"
}'
Node.js
const response = await fetch('https://api.postal.so/broadcasts', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Black Friday: acesso antecipado',
segment_id: '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
topic_id: '019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b',
from: 'Loja Exemplo <[email protected]>',
reply_to: '[email protected]',
subject: '{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você',
preheader: 'Até 40% de desconto a partir de quinta, só para clientes.',
language: 'pt-BR',
html: '<p>Oi, {{{FIRST_NAME|cliente}}}!</p><p>Separamos descontos de até 40% para quem é do plano {{{contact.plan}}}.</p><p><a href="https://lojaexemplo.com.br/black-friday">Ver as ofertas</a></p><p><a href="{{{POSTAL_UNSUBSCRIBE_URL}}}">Não quero mais receber</a></p>',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/broadcasts",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"name": "Black Friday: acesso antecipado",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"from": "Loja Exemplo <[email protected]>",
"reply_to": "[email protected]",
"subject": "{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você",
"preheader": "Até 40% de desconto a partir de quinta, só para clientes.",
"language": "pt-BR",
"html": "<p>Oi, {{{FIRST_NAME|cliente}}}!</p><p>Separamos descontos de até 40% para quem é do plano {{{contact.plan}}}.</p><p><a href=\"https://lojaexemplo.com.br/black-friday\">Ver as ofertas</a></p><p><a href=\"{{{POSTAL_UNSUBSCRIBE_URL}}}\">Não quero mais receber</a></p>",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('broadcasts', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'name' => 'Black Friday: acesso antecipado',
'segment_id' => '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
'topic_id' => '019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b',
'from' => 'Loja Exemplo <[email protected]>',
'reply_to' => '[email protected]',
'subject' => '{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você',
'preheader' => 'Até 40% de desconto a partir de quinta, só para clientes.',
'language' => 'pt-BR',
'html' => '<p>Oi, {{{FIRST_NAME|cliente}}}!</p><p>Separamos descontos de até 40% para quem é do plano {{{contact.plan}}}.</p><p><a href="https://lojaexemplo.com.br/black-friday">Ver as ofertas</a></p><p><a href="{{{POSTAL_UNSUBSCRIBE_URL}}}">Não quero mais receber</a></p>',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast",
"id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b"
}
#Obtener una campaña
GET/broadcasts/{id}
Devuelve la campaña con el contenido y las estadísticas. En stats, cada destinatario cuenta una vez por métrica (la primera apertura, el primer clic…): sent, delivered, opened, clicked, bounced, complained, unsubscribed y failed (incluye los suprimidos). recipients_count es el tamaño de la audiencia al inicio del envío, y processed_count, cuántos correos electrónicos ya se han creado. En una campaña pausada o que falló, error trae code, message y, cuando se reanuda sola, resumes_at.
Parámetros de ruta
idstringobligatorioID de la campaña.
Errores
- 404
not_foundEl recurso no existe o es de otro equipo.
cURL
curl https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast",
"id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"name": "Black Friday: acesso antecipado",
"status": "sent",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"segment": {
"id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"name": "Clientes ativos"
},
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"topic": {
"id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"name": "Novidades"
},
"template_id": null,
"from": "Loja Exemplo <[email protected]>",
"reply_to": ["[email protected]"],
"detect_replies": false,
"subject": "{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você",
"preheader": "Até 40% de desconto a partir de quinta, só para clientes.",
"language": null,
"html": "<p>Oi, {{{FIRST_NAME|cliente}}}!</p><p>Separamos descontos de até 40% para quem é do plano {{{contact.plan}}}.</p><p><a href=\"https://lojaexemplo.com.br/black-friday\">Ver as ofertas</a></p><p><a href=\"{{{POSTAL_UNSUBSCRIBE_URL}}}\">Não quero mais receber</a></p>",
"text": null,
"variables": {},
"editor": null,
"scheduled_at": null,
"schedule_timezone": null,
"queued_at": "2026-10-22T12:00:00.000000Z",
"sent_at": "2026-10-22T12:04:31.000000Z",
"canceled_at": null,
"recipients_count": 1247,
"processed_count": 1247,
"stats": {
"sent": 1247,
"delivered": 1236,
"opened": 612,
"clicked": 188,
"bounced": 6,
"complained": 1,
"unsubscribed": 9,
"failed": 0
},
"error": null,
"created_at": "2026-10-21T18:40:00.000000Z",
"updated_at": "2026-10-22T12:04:31.000000Z"
}
#Actualizar una campaña
PATCH/broadcasts/{id}
Solo los borradores y las campañas programadas pueden cambiar; los campos son los mismos de la creación, todos opcionales, y null limpia un campo. En una campaña programada, Postal vuelve a comprobar que sigue lista para enviarse.
Parámetros de ruta
idstringobligatorioID de la campaña.
Errores
- 422
validation_errorCampo no válido o, en una campaña programada, falta algo que exige el envío. - 409
invalid_broadcast_statusLa campaña ya está en cola, enviándose, enviada, en pausa, cancelada o falló. - 404
not_foundLa campaña, el segmento, el tema o la plantilla no existe.
cURL
curl -X PATCH https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": "{{{FIRST_NAME|Cliente}}}, faltam 3 dias para a Black Friday"
}'
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
subject: '{{{FIRST_NAME|Cliente}}}, faltam 3 dias para a Black Friday',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.patch(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"subject": "{{{FIRST_NAME|Cliente}}}, faltam 3 dias para a Black Friday",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->patch('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'subject' => '{{{FIRST_NAME|Cliente}}}, faltam 3 dias para a Black Friday',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast",
"id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b"
}
#Eliminar una campaña
DELETE/broadcasts/{id}
Solo borradores y campañas programadas. Para detener un envío en curso, cancélalo.
Parámetros de ruta
idstringobligatorioID de la campaña.
Errores
- 409
invalid_broadcast_statusLa campaña no está en un estado que permita esta acción. - 404
not_foundEl recurso no existe o es de otro equipo.
cURL
curl -X DELETE https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.delete(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->delete('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast",
"id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"deleted": true
}
#Calcular la audiencia
GET/broadcasts/audience
Cuántos contactos alcanzaría ahora una campaña para el segmento (y el tema). contacts es el total del segmento; recipients, quién la recibe de verdad; quota_remaining, cuántos correos electrónicos permite aún la cuota de envío (null: sin límite). Si recipients supera la cuota, el envío se pausa a la mitad y continúa cuando esta se renueve.
cycle_recipients indica lo que el envío haría con los contactos que recibieron campañas en el ciclo: new (cuántos de estos contactos aún no recibieron una campaña en el ciclo), used, limit, remaining, fits (false: el envío se rechazaría con 422 cycle_recipients_limit), billed_above_limit y resets_at. Con scheduled_at, la cuenta es la del ciclo de esa fecha.
Parámetros de consulta
segment_idstringobligatorioID del segmento.topic_idstringID del tema.scheduled_atstringFecha y hora de un envío programado (ISO 8601), para calcularcycle_recipientsen su ciclo.
Errores
- 422
validation_errorAlgún campo no es válido. El mensaje indica cuál. - 404
not_foundEl segmento o el tema no existe.
cURL
curl 'https://api.postal.so/broadcasts/audience?segment_id=019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a&topic_id=019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts/audience?segment_id=019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a&topic_id=019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/broadcasts/audience",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('broadcasts/audience', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'segment_id' => '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
'topic_id' => '019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast_audience",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"contacts": 1284,
"recipients": 1247,
"quota_remaining": 18793,
"cycle_recipients": {
"new": 412,
"used": 2870,
"limit": 5000,
"remaining": 2130,
"fits": true,
"billed_above_limit": false,
"resets_at": "2026-11-25T14:00:00Z"
}
}
#Vista previa
GET/broadcasts/{id}/preview
La campaña tal como la recibirá un contacto. Sin contact_id, las variables usan los valores por defecto y el enlace de baja se convierte en #. Funciona con borradores incompletos y muestra el borrador de una plantilla aún no publicada. El pie de baja automático, si lo hay, sale en el idioma que correspondería en el envío a ese contacto (reglas).
Parámetros de ruta
idstringobligatorioID de la campaña.
Parámetros de consulta
contact_idstringID o correo electrónico de un contacto para personalizar.
Errores
- 404
not_foundLa campaña o el contacto no existe.
cURL
curl 'https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/preview?contact_id=ana%40exemplo.com.br' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/preview?contact_id=ana%40exemplo.com.br', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/preview",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"contact_id": "[email protected]",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/preview', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'contact_id' => '[email protected]',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast_preview",
"broadcast_id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"subject": "Ana, a Black Friday começa mais cedo para você",
"preheader": "Até 40% de desconto a partir de quinta, só para clientes.",
"html": "<div style=\"display:none;…\">Até 40% de desconto a partir de quinta, só para clientes.…</div><p>Oi, Ana!</p><p>Separamos descontos de até 40% para quem é do plano pro.</p><p><a href=\"https://lojaexemplo.com.br/black-friday\">Ver as ofertas</a></p><p><a href=\"https://forms.postal.so/preferencias/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f?signature=8b2e…\">Não quero mais receber</a></p>",
"text": "Oi, Ana!\n\nSeparamos descontos de até 40% para quem é do plano pro.\n\nVer as ofertas (https://lojaexemplo.com.br/black-friday)\n\nNão quero mais receber (https://forms.postal.so/preferencias/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f?signature=8b2e…)"
}
#Enviar una prueba
POST/broadcasts/{id}/test
Envía la campaña tal como está ahora a un máximo de 5 direcciones, con [Test] antes del asunto ([Teste] si la solicitud envía Accept-Language: pt-BR) y la etiqueta broadcast_test. La personalización usa el contacto de contact_id o, sin él, el contacto con la primera dirección de to (si existe). El pie de baja automático sigue el idioma de la campaña o del contacto, como en un envío real (consulta Idioma del pie y de la página). Necesita un remitente de dominio verificado, asunto y contenido, pero no un segmento. Cuenta en la cuota de envío, no en las estadísticas.
Parámetros de ruta
idstringobligatorioID de la campaña.
Cuerpo de la solicitud
tostring[]obligatorioDe 1 a 5 direcciones.contact_idstringID o correo electrónico del contacto usado en la personalización.
Errores
- 422
validation_errorMás de 5 direcciones, dirección no válida, o falta el asunto, el contenido o el valor de una variable de la plantilla. - 403
validation_errorEl dominio del remitente no está verificado. - 403
sending_pausedLos envíos del equipo están en pausa. - 404
not_foundLa campaña o el contacto no existe. - 429
daily_quota_exceededSe agotó la cuota de envío (omonthly_quota_exceeded).
cURL
curl -X POST https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/test \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": ["[email protected]"],
"contact_id": "[email protected]"
}'
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: ['[email protected]'],
contact_id: '[email protected]',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/test",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"to": ["[email protected]"],
"contact_id": "[email protected]",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/test', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'to' => ['[email protected]'],
'contact_id' => '[email protected]',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "email",
"id": "019a1f02-4e5f-7a61-8c7d-8e9f0a1b2c3d"
}
#Enviar o programar
POST/broadcasts/{id}/send
Sin cuerpo, envía ahora (queued, luego sending). Con scheduled_at, programa (scheduled). El mismo endpoint reprograma o adelanta una campaña programada y reanuda una en pausa. Devuelve la campaña completa.
Antes de aceptar, Postal comprueba el segmento, el remitente (dominio verificado), el asunto, el contenido y las variables de la plantilla, y que haya al menos un destinatario. En un envío inmediato, comprueba también la cuota de envío; en una programación, solo comprueba si los envíos del equipo están en pausa.
También comprueba los contactos que recibieron campañas en el ciclo (en el de scheduled_at, si se programa): si los contactos de la campaña que aún no recibieron nada en el ciclo pasarían del límite de contactos, la campaña entera se rechaza con 422 cycle_recipients_limit y no se envía nada. Cuando va a salir, Postal comprueba de nuevo; si ya no cabe, queda paused con error.code cycle_recipients_limit y los números en error.cycle_recipients: sin nada enviado, espera que se envíe de nuevo o se cancele; a mitad del envío, el resto sale solo cuando se renueva el ciclo.
Parámetros de ruta
idstringobligatorioID de la campaña.
Cuerpo de la solicitud (opcional)
scheduled_atstringFecha y hora del envío, en ISO 8601 con zona horaria, como2026-10-28T09:00:00-03:00. En el futuro y hasta 30 días por delante.schedule_timezonestringZona horaria IANA en la que se eligió la hora, comoAmerica/Sao_Paulo. Solo sirve para mostrar la hora tal como se eligió (vuelve enschedule_timezone); no cambia cuándo sale el envío.
Errores
- 422
validation_errorFalta el segmento, el remitente, el asunto, el contenido o una variable de la plantilla; la plantilla no está publicada; ningún contacto del segmento puede recibir; oscheduled_atestá en el pasado o a más de 30 días. - 403
validation_errorEl dominio del remitente no está verificado. - 403
plan_limit_reachedEl plan no permite más campañas este mes. - 422
cycle_recipients_limitLos contactos de la campaña que aún no recibieron una campaña en el ciclo pasarían del límite de contactos del plan. No se envía nada;cycle_recipientstraenew,used,limit,remainingyresets_at. - 403
sending_pausedLos envíos del equipo están en pausa. - 403
paused_by_planEl segmento de la campaña está en pausa porque superó el límite del plan. Mejora tu plan o elimina otros segmentos. - 409
invalid_broadcast_statusLa campaña ya está en cola, enviándose, enviada, cancelada o falló. - 429
daily_quota_exceededEn un envío inmediato, se agotó la cuota de envío (omonthly_quota_exceeded). - 404
not_foundEl recurso no existe o es de otro equipo.
cURL
curl -X POST https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/send \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scheduled_at": "2026-10-28T09:00:00-03:00",
"schedule_timezone": "America/Sao_Paulo"
}'
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
scheduled_at: '2026-10-28T09:00:00-03:00',
schedule_timezone: 'America/Sao_Paulo',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/send",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"scheduled_at": "2026-10-28T09:00:00-03:00",
"schedule_timezone": "America/Sao_Paulo",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/send', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'scheduled_at' => '2026-10-28T09:00:00-03:00',
'schedule_timezone' => 'America/Sao_Paulo',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast",
"id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"name": "Black Friday: acesso antecipado",
"status": "scheduled",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"segment": {
"id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"name": "Clientes ativos"
},
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"topic": {
"id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"name": "Novidades"
},
"template_id": null,
"from": "Loja Exemplo <[email protected]>",
"reply_to": ["[email protected]"],
"detect_replies": false,
"subject": "{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você",
"preheader": "Até 40% de desconto a partir de quinta, só para clientes.",
"language": null,
"html": "<p>Oi, {{{FIRST_NAME|cliente}}}!</p><p>Separamos descontos de até 40% para quem é do plano {{{contact.plan}}}.</p><p><a href=\"https://lojaexemplo.com.br/black-friday\">Ver as ofertas</a></p><p><a href=\"{{{POSTAL_UNSUBSCRIBE_URL}}}\">Não quero mais receber</a></p>",
"text": null,
"variables": {},
"editor": null,
"scheduled_at": "2026-10-28T12:00:00.000000Z",
"schedule_timezone": "America/Sao_Paulo",
"queued_at": null,
"sent_at": null,
"canceled_at": null,
"recipients_count": 0,
"processed_count": 0,
"stats": {
"sent": 0,
"delivered": 0,
"opened": 0,
"clicked": 0,
"bounced": 0,
"complained": 0,
"unsubscribed": 0,
"failed": 0
},
"error": null,
"created_at": "2026-10-21T18:40:00.000000Z",
"updated_at": "2026-10-24T16:10:00.000000Z"
}
#Cancelar el envío
POST/broadcasts/{id}/cancel
Para una campaña programada, en cola, enviándose o en pausa. Los correos electrónicos que aún no habían salido quedan con estado canceled; los que ya salieron no se revierten. Devuelve la campaña completa.
Parámetros de ruta
idstringobligatorioID de la campaña.
Errores
- 409
invalid_broadcast_statusLa campaña es un borrador o ya terminó. - 404
not_foundEl recurso no existe o es de otro equipo.
cURL
curl -X POST https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/cancel \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/cancel', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/cancel",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/cancel', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast",
"id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"name": "Black Friday: acesso antecipado",
"status": "canceled",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"segment": {
"id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"name": "Clientes ativos"
},
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"topic": {
"id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"name": "Novidades"
},
"template_id": null,
"from": "Loja Exemplo <[email protected]>",
"reply_to": ["[email protected]"],
"detect_replies": false,
"subject": "{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você",
"preheader": "Até 40% de desconto a partir de quinta, só para clientes.",
"language": null,
"html": "<p>Oi, {{{FIRST_NAME|cliente}}}!</p><p>Separamos descontos de até 40% para quem é do plano {{{contact.plan}}}.</p><p><a href=\"https://lojaexemplo.com.br/black-friday\">Ver as ofertas</a></p><p><a href=\"{{{POSTAL_UNSUBSCRIBE_URL}}}\">Não quero mais receber</a></p>",
"text": null,
"variables": {},
"editor": null,
"scheduled_at": null,
"schedule_timezone": null,
"queued_at": "2026-10-22T12:00:00.000000Z",
"sent_at": null,
"canceled_at": "2026-10-22T12:02:10.000000Z",
"recipients_count": 1247,
"processed_count": 500,
"stats": {
"sent": 488,
"delivered": 471,
"opened": 96,
"clicked": 21,
"bounced": 2,
"complained": 0,
"unsubscribed": 1,
"failed": 0
},
"error": null,
"created_at": "2026-10-21T18:40:00.000000Z",
"updated_at": "2026-10-22T12:02:10.000000Z"
}
#Duplicar
POST/broadcasts/{id}/duplicate
Crea un borrador con el mismo contenido, idioma (language), segmento, tema y remitente, con “(copy)” en el nombre (“(cópia)” si la solicitud envía Accept-Language: pt-BR). Sirve para cualquier estado, incluso para reaprovechar una campaña enviada.
Parámetros de ruta
idstringobligatorioID de la campaña.
Errores
- 404
not_foundEl recurso no existe o es de otro equipo.
cURL
curl -X POST https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/duplicate \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/duplicate', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/duplicate",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/duplicate', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast",
"id": "019a1f05-3d4e-7f50-9b6c-7d8e9f0a1b2c"
}
#Listar destinatarios
GET/broadcasts/{id}/recipients
Cada destinatario con el estado del correo electrónico que recibió (el último evento: queued, sent, delivered, opened, clicked, bounced, complained, failed, suppressed, canceled…) y si canceló la suscripción desde esta campaña. id y email_id son el ID del correo electrónico, para usar en GET /emails/{id}.
Parámetros de ruta
idstringobligatorioID de la campaña.
Parámetros de consulta
limitintegerDe 1 a 100. Predeterminado: 20.afterstringCursor de la página siguiente.beforestringCursor de la página anterior.statusstringUn estado de correo electrónico, ounsubscribedpara quien canceló la suscripción.searchstringBusca en la dirección.
Errores
- 422
validation_errorEstado desconocido. - 404
not_foundEl recurso no existe o es de otro equipo.
cURL
curl 'https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/recipients?status=clicked' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/recipients?status=clicked', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/recipients",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"status": "clicked",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/recipients', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'status' => 'clicked',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "list",
"has_more": true,
"data": [
{
"object": "broadcast_recipient",
"id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
"email_id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
"contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"email": "[email protected]",
"first_name": "Ana",
"last_name": "Souza",
"status": "clicked",
"unsubscribed": false,
"error": null,
"sent_at": "2026-10-22T12:00:04.000000Z",
"created_at": "2026-10-22T12:00:02.000000Z"
}
]
}
#Métricas y webhooks
Además de stats, las métricas de correo aceptan la dimensión broadcast y el filtro broadcast_id, para comparar campañas por periodo, con tasas de apertura, clic y baja:
GET /emails/metrics?dimensions=broadcast&metrics=delivered,open_rate,click_rate,unsubscribe_rate&start_date=2026-10-01
{
"object": "metrics",
"start_date": "2026-10-01T00:00:00.000Z",
"end_date": "2026-10-24T16:10:00.000Z",
"metrics": [
"delivered",
"open_rate",
"click_rate",
"unsubscribe_rate"
],
"dimensions": ["broadcast"],
"granularity": "daily",
"totals": {
"delivered": 1236,
"open_rate": 0.4951,
"click_rate": 0.1521,
"unsubscribe_rate": 0.0073
},
"data": [
{
"broadcast_id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"broadcast_name": "Black Friday: acesso antecipado",
"delivered": 1236,
"open_rate": 0.4951,
"click_rate": 0.1521,
"unsubscribe_rate": 0.0073
}
]
}
Cuando se procesa al último destinatario, Postal dispara el webhook broadcast.sent. Los eventos de cada correo electrónico (email.delivered, email.opened…) traen "tags": {"broadcast_id": "…"}.
El número de campañas por mes puede tener un límite en tu plan de marketing (consulta Límites y broadcasts en GET /usage). Cuentan las campañas enviadas, en envío o programadas en el mes (en UTC).