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.

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.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.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.” O texto ganha uma linha com o link.
  • 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.

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

#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]"],
      "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.",
      "scheduled_at": 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.
  • 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.
  • 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.",
    "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.',
    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.",
        "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.',
        '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.

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]"],
  "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.",
  "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,
  "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.

Parâmetros de consulta

  • segment_idstringobrigatório
    ID do segmento.
  • topic_idstring
    ID do tópico.

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
}

#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.

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 [Teste] antes do assunto 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). 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.
  • 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.

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.

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.
  • 403sending_paused
    Os envios do time estão pausados.
  • 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"
  }'

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',
  }),
});

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",
    },
    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',
    ],
]);

$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]"],
  "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.",
  "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",
  "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]"],
  "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.",
  "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,
  "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, segmento, tópico e remetente, com “(cópia)” no nome. 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).