Iniciar sesión Crear cuenta
Marketing

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

  1. Crea la campaña como borrador (POST /broadcasts). Solo el nombre es obligatorio.
  2. Comprueba a cuántos contactos llega (GET /broadcasts/audience), mira la vista previa personalizada y envía una prueba.
  3. 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.
  4. 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

EstadoQué significaQué se puede hacer
draftBorrador. Puede quedar incompleta; lo que el envío exige se comprueba al enviar.Editar, eliminar, enviar, programar
scheduledProgramada 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
queuedEn cola para empezar el envío.Cancelar
sendingEnviando, en lotes de hasta 500 contactos. processed_count muestra el progreso.Cancelar
pausedDetenida 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
sentTodos los destinatarios se procesaron. Dispara el webhook broadcast.sent.Duplicar
canceledCancelada. Quien aún no había recibido ya no recibe.Duplicar
failedNo 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:

Asunto
{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você
VariableValor
{{{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-Unsubscribe y List-Unsubscribe-Post: List-Unsubscribe=One-Click van 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.unsubscribed de la campaña y dispara contact.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:

  1. el language de la campaña (pt-BR o en);
  2. la propiedad language del contacto, como pt-BR o en-US (vale el idioma anterior al guion; los demás idiomas se ignoran);
  3. la propiedad locale del contacto, de la misma forma;
  4. el idioma que el propietario del equipo eligió en el panel;
  5. 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, con reply.from, reply.subject, reply.preview y reply.auto.
  • Las respuestas automáticas (fuera de la oficina, autorrespondedores, avisos de entrega) llegan con reply.auto: true y 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

Clave de acceso total

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

  • limitinteger
    De 1 a 100. Predeterminado: 20.
  • afterstring
    Cursor de la página siguiente.
  • beforestring
    Cursor de la página anterior.
  • searchstring
    Busca en el nombre y en el asunto.
  • statusstring
    Uno o más estados separados por comas, como scheduled,sending.
GET /broadcasts

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);
200Respuesta
{
  "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

Clave de acceso total

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

  • namestringobligatorio
    Nombre interno, hasta 255 caracteres. Obligatorio al crear.
  • segment_idstring | null
    Segmento que recibe la campaña. Obligatorio para enviar.
  • topic_idstring | null
    Tema: solo recibe quien está suscrito a él, y el clic en “Cancelar suscripción” quita al contacto solo de este tema.
  • fromstring | null
    Remitente, como Loja Exemplo <[email protected]>, de un dominio verificado del equipo. Obligatorio para enviar.
  • reply_tostring | string[] | null
    Direcciones de respuesta.
  • detect_repliesboolean
    Por defecto: false. Con true, cada correo electrónico lleva una dirección de respuesta propia y Postal registra y reenvía las respuestas. Consulta Detectar respuestas.
  • subjectstring | null
    Asunto, hasta 998 caracteres, con variables. Obligatorio para enviar, a menos que la plantilla tenga asunto.
  • preheaderstring | null
    Texto de vista previa, hasta 500 caracteres.
  • languagestring | null
    Idioma de lo que Postal escribe en los correos electrónicos de esta campaña: pt-BR o en. 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 | null
    Cuerpo en HTML, hasta 2 MB. Para enviar, hace falta html, text o template_id.
  • textstring | null
    Versión en texto, hasta 2 MB.
  • template_idstring | null
    ID o alias de una plantilla. El contenido sale de la versión publicada (en lugar de html y text).
  • variablesobject | null
    Valores de las variables de la plantilla, como {"COUPON": "BF40"}.
  • editorobject | null
    Estado del editor visual del panel. Guárdalo y devuélvelo tal como llegó.

Errores

  • 422validation_error
    Campo no válido, como una dirección fuera del formato Nome <[email protected]> o un ID que no es UUID.
  • 404not_found
    El segmento, el tema o la plantilla no existe.
POST /broadcasts

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);
201Respuesta
{
  "object": "broadcast",
  "id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b"
}

#Obtener una campaña

GET/broadcasts/{id}

Clave de acceso total

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

  • idstringobligatorio
    ID de la campaña.

Errores

  • 404not_found
    El recurso no existe o es de otro equipo.
GET /broadcasts/{id}

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);
200Respuesta
{
  "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}

Clave de acceso total

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

  • idstringobligatorio
    ID de la campaña.

Errores

  • 422validation_error
    Campo no válido o, en una campaña programada, falta algo que exige el envío.
  • 409invalid_broadcast_status
    La campaña ya está en cola, enviándose, enviada, en pausa, cancelada o falló.
  • 404not_found
    La campaña, el segmento, el tema o la plantilla no existe.
PATCH /broadcasts/{id}

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);
200Respuesta
{
  "object": "broadcast",
  "id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b"
}

#Eliminar una campaña

DELETE/broadcasts/{id}

Clave de acceso total

Solo borradores y campañas programadas. Para detener un envío en curso, cancélalo.

Parámetros de ruta

  • idstringobligatorio
    ID de la campaña.

Errores

  • 409invalid_broadcast_status
    La campaña no está en un estado que permita esta acción.
  • 404not_found
    El recurso no existe o es de otro equipo.
DELETE /broadcasts/{id}

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);
200Respuesta
{
  "object": "broadcast",
  "id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
  "deleted": true
}

#Calcular la audiencia

GET/broadcasts/audience

Clave de acceso total

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_idstringobligatorio
    ID del segmento.
  • topic_idstring
    ID del tema.
  • scheduled_atstring
    Fecha y hora de un envío programado (ISO 8601), para calcular cycle_recipients en su ciclo.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 404not_found
    El segmento o el tema no existe.
GET /broadcasts/audience

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);
200Respuesta
{
  "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

Clave de acceso total

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

  • idstringobligatorio
    ID de la campaña.

Parámetros de consulta

  • contact_idstring
    ID o correo electrónico de un contacto para personalizar.

Errores

  • 404not_found
    La campaña o el contacto no existe.
GET /broadcasts/{id}/preview

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);
200Respuesta
{
  "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

Clave de acceso total

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

  • idstringobligatorio
    ID de la campaña.

Cuerpo de la solicitud

  • tostring[]obligatorio
    De 1 a 5 direcciones.
  • contact_idstring
    ID o correo electrónico del contacto usado en la personalización.

Errores

  • 422validation_error
    Más de 5 direcciones, dirección no válida, o falta el asunto, el contenido o el valor de una variable de la plantilla.
  • 403validation_error
    El dominio del remitente no está verificado.
  • 403sending_paused
    Los envíos del equipo están en pausa.
  • 404not_found
    La campaña o el contacto no existe.
  • 429daily_quota_exceeded
    Se agotó la cuota de envío (o monthly_quota_exceeded).
POST /broadcasts/{id}/test

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);
200Respuesta
{
  "object": "email",
  "id": "019a1f02-4e5f-7a61-8c7d-8e9f0a1b2c3d"
}

#Enviar o programar

POST/broadcasts/{id}/send

Clave de acceso total

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

  • idstringobligatorio
    ID de la campaña.

Cuerpo de la solicitud (opcional)

  • scheduled_atstring
    Fecha y hora del envío, en ISO 8601 con zona horaria, como 2026-10-28T09:00:00-03:00. En el futuro y hasta 30 días por delante.
  • schedule_timezonestring
    Zona horaria IANA en la que se eligió la hora, como America/Sao_Paulo. Solo sirve para mostrar la hora tal como se eligió (vuelve en schedule_timezone); no cambia cuándo sale el envío.

Errores

  • 422validation_error
    Falta 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; o scheduled_at está en el pasado o a más de 30 días.
  • 403validation_error
    El dominio del remitente no está verificado.
  • 403plan_limit_reached
    El plan no permite más campañas este mes.
  • 422cycle_recipients_limit
    Los 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_recipients trae new, used, limit, remaining y resets_at.
  • 403sending_paused
    Los envíos del equipo están en pausa.
  • 403paused_by_plan
    El segmento de la campaña está en pausa porque superó el límite del plan. Mejora tu plan o elimina otros segmentos.
  • 409invalid_broadcast_status
    La campaña ya está en cola, enviándose, enviada, cancelada o falló.
  • 429daily_quota_exceeded
    En un envío inmediato, se agotó la cuota de envío (o monthly_quota_exceeded).
  • 404not_found
    El recurso no existe o es de otro equipo.
POST /broadcasts/{id}/send

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);
200Respuesta
{
  "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

Clave de acceso total

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

  • idstringobligatorio
    ID de la campaña.

Errores

  • 409invalid_broadcast_status
    La campaña es un borrador o ya terminó.
  • 404not_found
    El recurso no existe o es de otro equipo.
POST /broadcasts/{id}/cancel

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);
200Respuesta
{
  "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

Clave de acceso total

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

  • idstringobligatorio
    ID de la campaña.

Errores

  • 404not_found
    El recurso no existe o es de otro equipo.
POST /broadcasts/{id}/duplicate

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);
201Respuesta
{
  "object": "broadcast",
  "id": "019a1f05-3d4e-7f50-9b6c-7d8e9f0a1b2c"
}

#Listar destinatarios

GET/broadcasts/{id}/recipients

Clave de acceso total

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

  • idstringobligatorio
    ID de la campaña.

Parámetros de consulta

  • limitinteger
    De 1 a 100. Predeterminado: 20.
  • afterstring
    Cursor de la página siguiente.
  • beforestring
    Cursor de la página anterior.
  • statusstring
    Un estado de correo electrónico, o unsubscribed para quien canceló la suscripción.
  • searchstring
    Busca en la dirección.

Errores

  • 422validation_error
    Estado desconocido.
  • 404not_found
    El recurso no existe o es de otro equipo.
GET /broadcasts/{id}/recipients

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);
200Respuesta
{
  "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:

Solicitud
GET /emails/metrics?dimensions=broadcast&metrics=delivered,open_rate,click_rate,unsubscribe_rate&start_date=2026-10-01
200Respuesta
{
  "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).