Entrar Criar conta
Marketing

Campanhas

E-mails de marketing para um segmento da audiência (endpoint /broadcasts), com personalização, teste, agendamento, descadastro e estatísticas.

Campanhas (endpoint /broadcasts) são envios de marketing para um segmento da sua audiência, opcionalmente só para quem está inscrito num tópico. Escreva o conteúdo na própria campanha (html e text) ou use um template publicado, personalize com os dados de cada contato, envie um teste, e envie na hora ou agende. Cada destinatário recebe o próprio e-mail, com um link de descadastro e os cabeçalhos List-Unsubscribe. Todos os endpoints exigem uma chave de acesso total.

#Como funciona

  1. Crie a campanha como rascunho (POST /broadcasts). Só o nome é obrigatório.
  2. Confira quantos contatos ela alcança (GET /broadcasts/audience), veja a prévia personalizada e mande um teste.
  3. Envie ou agende (POST /broadcasts/{id}/send). Nessa hora a Postal exige o segmento, um remetente de domínio verificado, o assunto e o conteúdo.
  4. Acompanhe o progresso e as estatísticas na própria campanha, em destinatários e nas métricas.

Quem recebe: os contatos do segmento que não estão descadastrados (unsubscribed), não estão na lista de supressão e, se houver topic_id, estão inscritos no tópico (escolheram receber ou, num tópico opt_in, não escolheram nada). A lista é lida em lotes durante o envio, então quem se descadastra no meio do caminho não recebe.

Num segmento dinâmico, os filtros são avaliados na hora do envio: quem bate com eles naquele momento recebe.

Cada destinatário vira um e-mail comum: conta na cota de envio, aparece em GET /emails e gera os webhooks email.* com a tag broadcast_id. Se a cota acabar no meio, a campanha fica paused e continua sozinha quando a cota renovar. Nenhum contato recebe a mesma campanha duas vezes.

Status

StatusO que significaO que dá para fazer
draftRascunho. Pode ficar incompleta; o que o envio exige é conferido ao enviar.Editar, excluir, enviar, agendar
scheduledAgendada para scheduled_at. Ainda pode ser editada, mas continua precisando estar pronta para envio. O fuso em que você escolheu o horário fica em schedule_timezone, só para exibição: o envio sai em scheduled_at.Editar, excluir, reagendar, enviar já, cancelar
queuedNa fila para começar o envio.Cancelar
sendingEnviando, em lotes de até 500 contatos. processed_count mostra o progresso.Cancelar
pausedParada no meio do envio, com o motivo em error. Com error.resumes_at (a cota acabou), volta sozinha quando a cota renova; sem ele (envios do time pausados), volta quando você enviar de novo. O error.code é daily_quota_exceeded ou monthly_quota_exceeded (a cota de envio acabou; scheduled_at mostra quando ela renova; a reserva do Adicional de envios não é usada por campanhas) ou sending_paused (os envios do time foram pausados pela reputação; o webhook sending.paused avisa, e o envio só é aceito de novo depois do sending.resumed).Enviar de novo, cancelar
sentTodos os destinatários foram processados. Dispara o webhook broadcast.sent.Duplicar
canceledCancelada. Quem ainda não tinha recebido não recebe mais.Duplicar
failedNão pôde ser enviada (por exemplo, o template deixou de estar publicado ou o domínio perdeu a verificação). error.code: invalid_broadcast ou send_failed.Duplicar

#Personalização

No assunto, no HTML e no texto, use variáveis entre chaves triplas. Depois de |, um valor padrão para quando o contato não tem o dado:

Assunto
{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você
VariávelValor
{{{FIRST_NAME}}}, {{{LAST_NAME}}}, {{{EMAIL}}}Nome, sobrenome e e-mail do contato. Também como {{{contact.first_name}}}, {{{contact.last_name}}} e {{{contact.email}}}.
{{{contact.plan}}}Qualquer propriedade de contato, pela chave. Sem valor no contato, usa o padrão inline ou o fallback_value da propriedade.
{{{COUPON}}}Variáveis do template (com template_id), preenchidas por variables ou pelo valor padrão declarado no template.
{{{POSTAL_UNSUBSCRIBE_URL}}}O link da página de preferências e descadastro do contato. Veja Descadastro.

A ordem é: o valor do contato, o padrão depois de |, o fallback_value da propriedade e, por fim, vazio. No HTML, os valores são escapados. O preheader entra como texto oculto no começo do corpo, o trecho que as caixas de entrada mostram ao lado do assunto. Sem text, a versão em texto é gerada a partir do HTML.

Com template_id, o conteúdo vem da versão publicada do template, e o subject da campanha, se houver, vale no lugar do assunto do template. Variáveis declaradas no template sem valor e sem padrão impedem o envio (422); FIRST_NAME, LAST_NAME, EMAIL e POSTAL_UNSUBSCRIBE_URL são preenchidas pelo contato.

#Descadastro

Todo e-mail de campanha leva um link de descadastro assinado, só daquele contato, na página de preferências em https://forms.postal.so/preferencias/…. Lá, a pessoa sai de todos os envios, volta a receber ou escolhe os tópicos públicos.

  • Coloque o link onde quiser com {{{POSTAL_UNSUBSCRIBE_URL}}}. Se o HTML não tiver a variável, a Postal acrescenta no fim um rodapé discreto: “Não quer mais receber estes e-mails? Descadastre-se.” (em inglês: “Do not want to receive these emails anymore? Unsubscribe.”). A versão em texto ganha uma linha com o link (“Para não receber mais estes e-mails: …” ou “To stop receiving these emails: …”). O idioma do rodapé segue as regras de Idioma do rodapé e da página.
  • Os cabeçalhos List-Unsubscribe e List-Unsubscribe-Post: List-Unsubscribe=One-Click vão em todos os e-mails, para o botão “Cancelar inscrição” das caixas de entrada. No clique, se a campanha tem tópico, o contato sai só do tópico; se não, é descadastrado de tudo.
  • Cada descadastro conta uma vez em stats.unsubscribed da campanha e dispara contact.updated.

Idioma do rodapé e da página

O rodapé automático e a página de preferências (com os avisos que ela mostra) falam o idioma de quem recebe, pt-BR ou en. A Postal escolhe para cada destinatário, usando o primeiro que existir:

  1. o language da campanha (pt-BR ou en);
  2. a propriedade language do contato, como pt-BR ou en-US (vale o idioma antes do hífen; outros idiomas são ignorados);
  3. a propriedade locale do contato, da mesma forma;
  4. o idioma que o dono do time escolheu no painel;
  5. português do Brasil, quando nada diz qual é o idioma.

A página de preferências usa a mesma ordem. A campanha que vale é a do e-mail em que a pessoa clicou (o link leva o ID do e-mail); links de automações e links antigos, sem o e-mail da campanha, começam pelo contato. O Accept-Language do navegador não conta nessas páginas. A página de link inválido (link alterado, ou de um contato que não existe) escolhe assim: o idioma do contato citado no link (o das propriedades ou, sem ele, o do time), depois o Accept-Language do navegador (pt-BR ou en) e, por fim, português do Brasil.

Para fixar o idioma de uma campanha, envie language ao criá-la ou atualizá-la. A Postal não traduz o seu conteúdo (assunto, HTML, texto): só o que ela mesma acrescenta.

Detalhes e boas práticas no guia Marketing por e-mail.

#Detectar respostas

Com detect_replies: true, cada e-mail da campanha sai com um Reply-To próprio, no formato r-<token>@…, que vale só para aquele e-mail: no domínio do remetente, se ele recebe e-mails pela Postal, ou no domínio de recebimento da Postal. Quando a pessoa responde, a Postal registra a resposta no e-mail e a encaminha para o seu reply_to (ou, sem ele, para o remetente from), então você continua recebendo as respostas normalmente.

  • A resposta dispara o webhook email.replied, com reply.from, reply.subject, reply.preview e reply.auto.
  • Respostas automáticas (fora do escritório, auto-responders, avisos de entrega) vêm com reply.auto: true e não contam como resposta.
  • Cada e-mail encaminha no máximo 20 respostas, para dois auto-responders não ficarem trocando mensagens para sempre.

As automações têm a mesma opção no passo send_email, e o passo wait_for pode esperar a resposta.

#Listar campanhas

GET/broadcasts

Chave de acesso total

Do mais recente para o mais antigo. A lista traz o resumo, sem html, text, variables e editor.

Parâmetros de consulta

  • limitinteger
    De 1 a 100. Padrão: 20.
  • afterstring
    Cursor da próxima página.
  • beforestring
    Cursor da página anterior.
  • searchstring
    Busca no nome e no assunto.
  • statusstring
    Um ou mais status separados por vírgula, 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);
200Resposta
{
  "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"
    }
  ]
}

#Criar uma campanha

POST/broadcasts

Chave de acesso total

Cria um rascunho (draft). Pode faltar tudo menos o nome: o que o envio exige é conferido no envio. O segmento, o tópico e o template precisam ser do seu time.

Corpo da requisição

  • namestringobrigatório
    Nome interno, até 255 caracteres. Obrigatório na criação.
  • segment_idstring | null
    Segmento que recebe a campanha. Obrigatório para enviar.
  • topic_idstring | null
    Tópico: só recebe quem está inscrito nele, e o clique em “Cancelar inscrição” tira o contato só deste tópico.
  • fromstring | null
    Remetente, como Loja Exemplo <[email protected]>, de um domínio verificado do time. Obrigatório para enviar.
  • reply_tostring | string[] | null
    Endereços para resposta.
  • detect_repliesboolean
    Padrão: false. Com true, cada e-mail leva um endereço de resposta próprio e a Postal registra e encaminha as respostas. Veja Detectar respostas.
  • subjectstring | null
    Assunto, até 998 caracteres, com variáveis. Obrigatório para enviar, a não ser que o template tenha assunto.
  • preheaderstring | null
    Texto de pré-visualização, até 500 caracteres.
  • languagestring | null
    Idioma do que a Postal escreve nos e-mails desta campanha: pt-BR ou en. Vale para o rodapé de descadastro automático e para a página de preferências que o link abre; null (padrão) deixa a Postal escolher por destinatário. Veja Descadastro. Não traduz o seu conteúdo.
  • htmlstring | null
    Corpo em HTML, até 2 MB. Para enviar, é preciso html, text ou template_id.
  • textstring | null
    Versão em texto, até 2 MB.
  • template_idstring | null
    ID ou alias de um template. O conteúdo vem da versão publicada (no lugar de html e text).
  • variablesobject | null
    Valores das variáveis do template, como {"COUPON": "BF40"}.
  • editorobject | null
    Estado do editor visual do painel. Guarde e devolva como veio.

Erros

  • 422validation_error
    Campo inválido, como um endereço fora do formato Nome <[email protected]> ou um ID que não é UUID.
  • 404not_found
    O segmento, o tópico ou o template não 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);
201Resposta
{
  "object": "broadcast",
  "id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b"
}

#Obter uma campanha

GET/broadcasts/{id}

Chave de acesso total

Devolve a campanha com o conteúdo e as estatísticas. Em stats, cada destinatário conta uma vez por métrica (a primeira abertura, o primeiro clique…): sent, delivered, opened, clicked, bounced, complained, unsubscribed e failed (inclui os suprimidos). recipients_count é o tamanho da audiência no início do envio, e processed_count, quantos e-mails já foram criados. Numa campanha pausada ou que falhou, error traz code, message e, quando ela volta sozinha, resumes_at.

Parâmetros de caminho

  • idstringobrigatório
    ID da campanha.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
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);
200Resposta
{
  "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"
}

#Atualizar uma campanha

PATCH/broadcasts/{id}

Chave de acesso total

Só rascunhos e campanhas agendadas podem mudar; os campos são os mesmos da criação, todos opcionais, e null limpa um campo. Numa campanha agendada, a Postal confere de novo se ela continua pronta para envio.

Parâmetros de caminho

  • idstringobrigatório
    ID da campanha.

Erros

  • 422validation_error
    Campo inválido ou, numa campanha agendada, algo que o envio exige ficou faltando.
  • 409invalid_broadcast_status
    A campanha já está na fila, enviando, enviada, pausada, cancelada ou falhou.
  • 404not_found
    A campanha, o segmento, o tópico ou o template não 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);
200Resposta
{
  "object": "broadcast",
  "id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b"
}

#Excluir uma campanha

DELETE/broadcasts/{id}

Chave de acesso total

Só rascunhos e campanhas agendadas. Para parar um envio em andamento, cancele.

Parâmetros de caminho

  • idstringobrigatório
    ID da campanha.

Erros

  • 409invalid_broadcast_status
    A campanha não está num status que permite esta ação.
  • 404not_found
    O recurso não existe ou é de outro time.
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);
200Resposta
{
  "object": "broadcast",
  "id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
  "deleted": true
}

#Calcular a audiência

GET/broadcasts/audience

Chave de acesso total

Quantos contatos uma campanha para o segmento (e o tópico) alcançaria agora. contacts é o total do segmento; recipients, quem de fato recebe; quota_remaining, quantos e-mails a cota de envio ainda permite (null: sem limite). Se recipients passar da cota, o envio pausa no meio e continua quando ela renovar.

cycle_recipients diz o que o envio faria com os contatos que receberam campanhas no ciclo: new (quantos destes contatos ainda não receberam campanha no ciclo), used, limit, remaining, fits (false: o envio seria recusado com 422 cycle_recipients_limit), billed_above_limit e resets_at. Com scheduled_at, a conta é a do ciclo dessa data.

Parâmetros de consulta

  • segment_idstringobrigatório
    ID do segmento.
  • topic_idstring
    ID do tópico.
  • scheduled_atstring
    Data e hora de um envio agendado (ISO 8601), para calcular cycle_recipients no ciclo dela.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 404not_found
    O segmento ou o tópico não 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);
200Resposta
{
  "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"
  }
}

#Pré-visualizar

GET/broadcasts/{id}/preview

Chave de acesso total

A campanha como um contato vai receber. Sem contact_id, as variáveis usam os valores padrão e o link de descadastro vira #. Funciona com rascunhos incompletos e mostra o rascunho de um template ainda não publicado. O rodapé de descadastro automático, se houver, sai no idioma que valeria no envio para aquele contato (regras).

Parâmetros de caminho

  • idstringobrigatório
    ID da campanha.

Parâmetros de consulta

  • contact_idstring
    ID ou e-mail de um contato para personalizar.

Erros

  • 404not_found
    A campanha ou o contato não 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);
200Resposta
{
  "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 um teste

POST/broadcasts/{id}/test

Chave de acesso total

Envia a campanha como está agora para até 5 endereços, com [Test] antes do assunto ([Teste] se a requisição enviar Accept-Language: pt-BR) e a tag broadcast_test. A personalização usa o contato de contact_id ou, sem ele, o contato com o primeiro endereço de to (se existir). O rodapé de descadastro automático segue o idioma da campanha ou do contato, como num envio de verdade (veja Idioma do rodapé e da página). Precisa de remetente de domínio verificado, assunto e conteúdo, mas não de segmento. Conta na cota de envio, não nas estatísticas.

Parâmetros de caminho

  • idstringobrigatório
    ID da campanha.

Corpo da requisição

  • tostring[]obrigatório
    De 1 a 5 endereços.
  • contact_idstring
    ID ou e-mail do contato usado na personalização.

Erros

  • 422validation_error
    Mais de 5 endereços, endereço inválido, ou falta assunto, conteúdo ou valor de variável do template.
  • 403validation_error
    O domínio do remetente não está verificado.
  • 403sending_paused
    Os envios do time estão pausados.
  • 404not_found
    A campanha ou o contato não existe.
  • 429daily_quota_exceeded
    A cota de envio acabou (ou 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);
200Resposta
{
  "object": "email",
  "id": "019a1f02-4e5f-7a61-8c7d-8e9f0a1b2c3d"
}

#Enviar ou agendar

POST/broadcasts/{id}/send

Chave de acesso total

Sem corpo, envia agora (queued, depois sending). Com scheduled_at, agenda (scheduled). O mesmo endpoint reagenda ou antecipa uma campanha agendada e retoma uma pausada. Devolve a campanha completa.

Antes de aceitar, a Postal confere o segmento, o remetente (domínio verificado), o assunto, o conteúdo e as variáveis do template, e se há ao menos um destinatário. Num envio imediato, confere também a cota de envio; num agendamento, só confere se os envios do time estão pausados.

Confere também os contatos que receberam campanhas no ciclo (no de scheduled_at, num agendamento): se os contatos da campanha que ainda não receberam nada no ciclo passariam do limite de contatos, a campanha inteira é recusada com 422 cycle_recipients_limit e nada é enviado. Quando ela vai sair, a Postal confere de novo; se não couber mais, ela fica paused com error.code cycle_recipients_limit e os números em error.cycle_recipients: sem nada enviado, espera ser enviada de novo ou cancelada; no meio do envio, o resto sai sozinho quando o ciclo renova.

Parâmetros de caminho

  • idstringobrigatório
    ID da campanha.

Corpo da requisição (opcional)

  • scheduled_atstring
    Data e hora do envio, em ISO 8601 com fuso, como 2026-10-28T09:00:00-03:00. No futuro e até 30 dias à frente.
  • schedule_timezonestring
    Fuso IANA em que o horário foi escolhido, como America/Sao_Paulo. Só serve para mostrar o horário como foi escolhido (volta em schedule_timezone); não muda quando o envio sai.

Erros

  • 422validation_error
    Falta segmento, remetente, assunto, conteúdo ou variável do template; o template não está publicado; nenhum contato do segmento pode receber; ou scheduled_at está no passado ou a mais de 30 dias.
  • 403validation_error
    O domínio do remetente não está verificado.
  • 403plan_limit_reached
    O plano não permite mais campanhas neste mês.
  • 422cycle_recipients_limit
    Os contatos da campanha que ainda não receberam campanha no ciclo passariam do limite de contatos do plano. Nada é enviado; cycle_recipients traz new, used, limit, remaining e resets_at.
  • 403sending_paused
    Os envios do time estão pausados.
  • 403paused_by_plan
    O segmento da campanha está pausado porque passou do limite do plano. Faça upgrade ou apague outros segmentos.
  • 409invalid_broadcast_status
    A campanha já está na fila, enviando, enviada, cancelada ou falhou.
  • 429daily_quota_exceeded
    Num envio imediato, a cota de envio acabou (ou monthly_quota_exceeded).
  • 404not_found
    O recurso não existe ou é de outro time.
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);
200Resposta
{
  "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 o envio

POST/broadcasts/{id}/cancel

Chave de acesso total

Para uma campanha agendada, na fila, enviando ou pausada. Os e-mails que ainda não tinham saído ficam com status canceled; os que já saíram não voltam. Devolve a campanha completa.

Parâmetros de caminho

  • idstringobrigatório
    ID da campanha.

Erros

  • 409invalid_broadcast_status
    A campanha é um rascunho ou já terminou.
  • 404not_found
    O recurso não existe ou é de outro time.
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);
200Resposta
{
  "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

Chave de acesso total

Cria um rascunho com o mesmo conteúdo, idioma (language), segmento, tópico e remetente, com “(copy)” no nome (“(cópia)” se a requisição enviar Accept-Language: pt-BR). Serve para qualquer status, inclusive para reaproveitar uma campanha enviada.

Parâmetros de caminho

  • idstringobrigatório
    ID da campanha.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
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);
201Resposta
{
  "object": "broadcast",
  "id": "019a1f05-3d4e-7f50-9b6c-7d8e9f0a1b2c"
}

#Listar destinatários

GET/broadcasts/{id}/recipients

Chave de acesso total

Cada destinatário com o status do e-mail que recebeu (o último evento: queued, sent, delivered, opened, clicked, bounced, complained, failed, suppressed, canceled…) e se ele se descadastrou por esta campanha. id e email_id são o ID do e-mail, para usar em GET /emails/{id}.

Parâmetros de caminho

  • idstringobrigatório
    ID da campanha.

Parâmetros de consulta

  • limitinteger
    De 1 a 100. Padrão: 20.
  • afterstring
    Cursor da próxima página.
  • beforestring
    Cursor da página anterior.
  • statusstring
    Um status de e-mail, ou unsubscribed para quem se descadastrou.
  • searchstring
    Busca no endereço.

Erros

  • 422validation_error
    Status desconhecido.
  • 404not_found
    O recurso não existe ou é de outro time.
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);
200Resposta
{
  "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 e webhooks

Além de stats, as métricas de e-mail aceitam a dimensão broadcast e o filtro broadcast_id, para comparar campanhas por período, com taxas de abertura, clique e descadastro:

Requisição
GET /emails/metrics?dimensions=broadcast&metrics=delivered,open_rate,click_rate,unsubscribe_rate&start_date=2026-10-01
200Resposta
{
  "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
    }
  ]
}

Quando o último destinatário é processado, a Postal dispara o webhook broadcast.sent. Os eventos de cada e-mail (email.delivered, email.opened…) trazem "tags": {"broadcast_id": "…"}.

O número de campanhas por mês pode ter limite no seu plano de marketing (veja Limites e broadcasts em GET /usage). Contam as campanhas enviadas, enviando ou agendadas no mês (em UTC).