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
- Crie a campanha como rascunho (
POST /broadcasts). Só o nome é obrigatório. - Confira quantos contatos ela alcança (
GET /broadcasts/audience), veja a prévia personalizada e mande um teste. - 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. - 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
| Status | O que significa | O que dá para fazer |
|---|---|---|
draft | Rascunho. Pode ficar incompleta; o que o envio exige é conferido ao enviar. | Editar, excluir, enviar, agendar |
scheduled | Agendada 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 |
queued | Na fila para começar o envio. | Cancelar |
sending | Enviando, em lotes de até 500 contatos. processed_count mostra o progresso. | Cancelar |
paused | Parada 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 |
sent | Todos os destinatários foram processados. Dispara o webhook broadcast.sent. | Duplicar |
canceled | Cancelada. Quem ainda não tinha recebido não recebe mais. | Duplicar |
failed | Nã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:
{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você
| Variável | Valor |
|---|---|
{{{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-UnsubscribeeList-Unsubscribe-Post: List-Unsubscribe=One-Clickvã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.unsubscribedda campanha e disparacontact.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:
- o
languageda campanha (pt-BRouen); - a propriedade
languagedo contato, comopt-BRouen-US(vale o idioma antes do hífen; outros idiomas são ignorados); - a propriedade
localedo contato, da mesma forma; - o idioma que o dono do time escolheu no painel;
- 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, comreply.from,reply.subject,reply.previewereply.auto. - Respostas automáticas (fora do escritório, auto-responders, avisos de entrega) vêm com
reply.auto: truee 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
Do mais recente para o mais antigo. A lista traz o resumo, sem html, text, variables e editor.
Parâmetros de consulta
limitintegerDe 1 a 100. Padrão: 20.afterstringCursor da próxima página.beforestringCursor da página anterior.searchstringBusca no nome e no assunto.statusstringUm ou mais status separados por vírgula, comoscheduled,sending.
cURL
curl 'https://api.postal.so/broadcasts?status=sent' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts?status=sent', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/broadcasts",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"status": "sent",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('broadcasts', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'status' => 'sent',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "list",
"has_more": false,
"data": [
{
"object": "broadcast",
"id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"name": "Black Friday: acesso antecipado",
"status": "sent",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"segment": {
"id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"name": "Clientes ativos"
},
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"topic": {
"id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"name": "Novidades"
},
"template_id": null,
"from": "Loja Exemplo <[email protected]>",
"reply_to": ["[email protected]"],
"detect_replies": false,
"subject": "{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você",
"preheader": "Até 40% de desconto a partir de quinta, só para clientes.",
"language": null,
"scheduled_at": null,
"schedule_timezone": null,
"queued_at": "2026-10-22T12:00:00.000000Z",
"sent_at": "2026-10-22T12:04:31.000000Z",
"canceled_at": null,
"recipients_count": 1247,
"processed_count": 1247,
"stats": {
"sent": 1247,
"delivered": 1236,
"opened": 612,
"clicked": 188,
"bounced": 6,
"complained": 1,
"unsubscribed": 9,
"failed": 0
},
"error": null,
"created_at": "2026-10-21T18:40:00.000000Z",
"updated_at": "2026-10-22T12:04:31.000000Z"
}
]
}
#Criar uma campanha
POST/broadcasts
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órioNome interno, até 255 caracteres. Obrigatório na criação.segment_idstring | nullSegmento que recebe a campanha. Obrigatório para enviar.topic_idstring | nullTópico: só recebe quem está inscrito nele, e o clique em “Cancelar inscrição” tira o contato só deste tópico.fromstring | nullRemetente, comoLoja Exemplo <[email protected]>, de um domínio verificado do time. Obrigatório para enviar.reply_tostring | string[] | nullEndereços para resposta.detect_repliesbooleanPadrão:false. Comtrue, cada e-mail leva um endereço de resposta próprio e a Postal registra e encaminha as respostas. Veja Detectar respostas.subjectstring | nullAssunto, até 998 caracteres, com variáveis. Obrigatório para enviar, a não ser que o template tenha assunto.preheaderstring | nullTexto de pré-visualização, até 500 caracteres.languagestring | nullIdioma do que a Postal escreve nos e-mails desta campanha:pt-BRouen. 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 | nullCorpo em HTML, até 2 MB. Para enviar, é precisohtml,textoutemplate_id.textstring | nullVersão em texto, até 2 MB.template_idstring | nullvariablesobject | nullValores das variáveis do template, como{"COUPON": "BF40"}.editorobject | nullEstado do editor visual do painel. Guarde e devolva como veio.
Erros
- 422
validation_errorCampo inválido, como um endereço fora do formatoNome <[email protected]>ou um ID que não é UUID. - 404
not_foundO segmento, o tópico ou o template não existe.
cURL
curl -X POST https://api.postal.so/broadcasts \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Black Friday: acesso antecipado",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"from": "Loja Exemplo <[email protected]>",
"reply_to": "[email protected]",
"subject": "{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você",
"preheader": "Até 40% de desconto a partir de quinta, só para clientes.",
"language": "pt-BR",
"html": "<p>Oi, {{{FIRST_NAME|cliente}}}!</p><p>Separamos descontos de até 40% para quem é do plano {{{contact.plan}}}.</p><p><a href=\"https://lojaexemplo.com.br/black-friday\">Ver as ofertas</a></p><p><a href=\"{{{POSTAL_UNSUBSCRIBE_URL}}}\">Não quero mais receber</a></p>"
}'
Node.js
const response = await fetch('https://api.postal.so/broadcasts', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Black Friday: acesso antecipado',
segment_id: '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
topic_id: '019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b',
from: 'Loja Exemplo <[email protected]>',
reply_to: '[email protected]',
subject: '{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você',
preheader: 'Até 40% de desconto a partir de quinta, só para clientes.',
language: 'pt-BR',
html: '<p>Oi, {{{FIRST_NAME|cliente}}}!</p><p>Separamos descontos de até 40% para quem é do plano {{{contact.plan}}}.</p><p><a href="https://lojaexemplo.com.br/black-friday">Ver as ofertas</a></p><p><a href="{{{POSTAL_UNSUBSCRIBE_URL}}}">Não quero mais receber</a></p>',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/broadcasts",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"name": "Black Friday: acesso antecipado",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"from": "Loja Exemplo <[email protected]>",
"reply_to": "[email protected]",
"subject": "{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você",
"preheader": "Até 40% de desconto a partir de quinta, só para clientes.",
"language": "pt-BR",
"html": "<p>Oi, {{{FIRST_NAME|cliente}}}!</p><p>Separamos descontos de até 40% para quem é do plano {{{contact.plan}}}.</p><p><a href=\"https://lojaexemplo.com.br/black-friday\">Ver as ofertas</a></p><p><a href=\"{{{POSTAL_UNSUBSCRIBE_URL}}}\">Não quero mais receber</a></p>",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('broadcasts', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'name' => 'Black Friday: acesso antecipado',
'segment_id' => '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
'topic_id' => '019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b',
'from' => 'Loja Exemplo <[email protected]>',
'reply_to' => '[email protected]',
'subject' => '{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você',
'preheader' => 'Até 40% de desconto a partir de quinta, só para clientes.',
'language' => 'pt-BR',
'html' => '<p>Oi, {{{FIRST_NAME|cliente}}}!</p><p>Separamos descontos de até 40% para quem é do plano {{{contact.plan}}}.</p><p><a href="https://lojaexemplo.com.br/black-friday">Ver as ofertas</a></p><p><a href="{{{POSTAL_UNSUBSCRIBE_URL}}}">Não quero mais receber</a></p>',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast",
"id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b"
}
#Obter uma campanha
GET/broadcasts/{id}
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órioID da campanha.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
cURL
curl https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast",
"id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"name": "Black Friday: acesso antecipado",
"status": "sent",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"segment": {
"id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"name": "Clientes ativos"
},
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"topic": {
"id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"name": "Novidades"
},
"template_id": null,
"from": "Loja Exemplo <[email protected]>",
"reply_to": ["[email protected]"],
"detect_replies": false,
"subject": "{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você",
"preheader": "Até 40% de desconto a partir de quinta, só para clientes.",
"language": null,
"html": "<p>Oi, {{{FIRST_NAME|cliente}}}!</p><p>Separamos descontos de até 40% para quem é do plano {{{contact.plan}}}.</p><p><a href=\"https://lojaexemplo.com.br/black-friday\">Ver as ofertas</a></p><p><a href=\"{{{POSTAL_UNSUBSCRIBE_URL}}}\">Não quero mais receber</a></p>",
"text": null,
"variables": {},
"editor": null,
"scheduled_at": null,
"schedule_timezone": null,
"queued_at": "2026-10-22T12:00:00.000000Z",
"sent_at": "2026-10-22T12:04:31.000000Z",
"canceled_at": null,
"recipients_count": 1247,
"processed_count": 1247,
"stats": {
"sent": 1247,
"delivered": 1236,
"opened": 612,
"clicked": 188,
"bounced": 6,
"complained": 1,
"unsubscribed": 9,
"failed": 0
},
"error": null,
"created_at": "2026-10-21T18:40:00.000000Z",
"updated_at": "2026-10-22T12:04:31.000000Z"
}
#Atualizar uma campanha
PATCH/broadcasts/{id}
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órioID da campanha.
Erros
- 422
validation_errorCampo inválido ou, numa campanha agendada, algo que o envio exige ficou faltando. - 409
invalid_broadcast_statusA campanha já está na fila, enviando, enviada, pausada, cancelada ou falhou. - 404
not_foundA campanha, o segmento, o tópico ou o template não existe.
cURL
curl -X PATCH https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": "{{{FIRST_NAME|Cliente}}}, faltam 3 dias para a Black Friday"
}'
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
subject: '{{{FIRST_NAME|Cliente}}}, faltam 3 dias para a Black Friday',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.patch(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"subject": "{{{FIRST_NAME|Cliente}}}, faltam 3 dias para a Black Friday",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->patch('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'subject' => '{{{FIRST_NAME|Cliente}}}, faltam 3 dias para a Black Friday',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast",
"id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b"
}
#Excluir uma campanha
DELETE/broadcasts/{id}
Só rascunhos e campanhas agendadas. Para parar um envio em andamento, cancele.
Parâmetros de caminho
idstringobrigatórioID da campanha.
Erros
- 409
invalid_broadcast_statusA campanha não está num status que permite esta ação. - 404
not_foundO recurso não existe ou é de outro time.
cURL
curl -X DELETE https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.delete(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->delete('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast",
"id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"deleted": true
}
#Calcular a audiência
GET/broadcasts/audience
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órioID do segmento.topic_idstringID do tópico.scheduled_atstringData e hora de um envio agendado (ISO 8601), para calcularcycle_recipientsno ciclo dela.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual. - 404
not_foundO segmento ou o tópico não existe.
cURL
curl 'https://api.postal.so/broadcasts/audience?segment_id=019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a&topic_id=019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts/audience?segment_id=019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a&topic_id=019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/broadcasts/audience",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('broadcasts/audience', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'segment_id' => '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
'topic_id' => '019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast_audience",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"contacts": 1284,
"recipients": 1247,
"quota_remaining": 18793,
"cycle_recipients": {
"new": 412,
"used": 2870,
"limit": 5000,
"remaining": 2130,
"fits": true,
"billed_above_limit": false,
"resets_at": "2026-11-25T14:00:00Z"
}
}
#Pré-visualizar
GET/broadcasts/{id}/preview
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órioID da campanha.
Parâmetros de consulta
contact_idstringID ou e-mail de um contato para personalizar.
Erros
- 404
not_foundA campanha ou o contato não existe.
cURL
curl 'https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/preview?contact_id=ana%40exemplo.com.br' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/preview?contact_id=ana%40exemplo.com.br', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/preview",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"contact_id": "[email protected]",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/preview', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'contact_id' => '[email protected]',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast_preview",
"broadcast_id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"subject": "Ana, a Black Friday começa mais cedo para você",
"preheader": "Até 40% de desconto a partir de quinta, só para clientes.",
"html": "<div style=\"display:none;…\">Até 40% de desconto a partir de quinta, só para clientes.…</div><p>Oi, Ana!</p><p>Separamos descontos de até 40% para quem é do plano pro.</p><p><a href=\"https://lojaexemplo.com.br/black-friday\">Ver as ofertas</a></p><p><a href=\"https://forms.postal.so/preferencias/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f?signature=8b2e…\">Não quero mais receber</a></p>",
"text": "Oi, Ana!\n\nSeparamos descontos de até 40% para quem é do plano pro.\n\nVer as ofertas (https://lojaexemplo.com.br/black-friday)\n\nNão quero mais receber (https://forms.postal.so/preferencias/019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f?signature=8b2e…)"
}
#Enviar um teste
POST/broadcasts/{id}/test
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órioID da campanha.
Corpo da requisição
tostring[]obrigatórioDe 1 a 5 endereços.contact_idstringID ou e-mail do contato usado na personalização.
Erros
- 422
validation_errorMais de 5 endereços, endereço inválido, ou falta assunto, conteúdo ou valor de variável do template. - 403
validation_errorO domínio do remetente não está verificado. - 403
sending_pausedOs envios do time estão pausados. - 404
not_foundA campanha ou o contato não existe. - 429
daily_quota_exceededA cota de envio acabou (oumonthly_quota_exceeded).
cURL
curl -X POST https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/test \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": ["[email protected]"],
"contact_id": "[email protected]"
}'
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: ['[email protected]'],
contact_id: '[email protected]',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/test",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"to": ["[email protected]"],
"contact_id": "[email protected]",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/test', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'to' => ['[email protected]'],
'contact_id' => '[email protected]',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "email",
"id": "019a1f02-4e5f-7a61-8c7d-8e9f0a1b2c3d"
}
#Enviar ou agendar
POST/broadcasts/{id}/send
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órioID da campanha.
Corpo da requisição (opcional)
scheduled_atstringData e hora do envio, em ISO 8601 com fuso, como2026-10-28T09:00:00-03:00. No futuro e até 30 dias à frente.schedule_timezonestringFuso IANA em que o horário foi escolhido, comoAmerica/Sao_Paulo. Só serve para mostrar o horário como foi escolhido (volta emschedule_timezone); não muda quando o envio sai.
Erros
- 422
validation_errorFalta segmento, remetente, assunto, conteúdo ou variável do template; o template não está publicado; nenhum contato do segmento pode receber; ouscheduled_atestá no passado ou a mais de 30 dias. - 403
validation_errorO domínio do remetente não está verificado. - 403
plan_limit_reachedO plano não permite mais campanhas neste mês. - 422
cycle_recipients_limitOs contatos da campanha que ainda não receberam campanha no ciclo passariam do limite de contatos do plano. Nada é enviado;cycle_recipientstraznew,used,limit,remainingeresets_at. - 403
sending_pausedOs envios do time estão pausados. - 403
paused_by_planO segmento da campanha está pausado porque passou do limite do plano. Faça upgrade ou apague outros segmentos. - 409
invalid_broadcast_statusA campanha já está na fila, enviando, enviada, cancelada ou falhou. - 429
daily_quota_exceededNum envio imediato, a cota de envio acabou (oumonthly_quota_exceeded). - 404
not_foundO recurso não existe ou é de outro time.
cURL
curl -X POST https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/send \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scheduled_at": "2026-10-28T09:00:00-03:00",
"schedule_timezone": "America/Sao_Paulo"
}'
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
scheduled_at: '2026-10-28T09:00:00-03:00',
schedule_timezone: 'America/Sao_Paulo',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/send",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"scheduled_at": "2026-10-28T09:00:00-03:00",
"schedule_timezone": "America/Sao_Paulo",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/send', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'scheduled_at' => '2026-10-28T09:00:00-03:00',
'schedule_timezone' => 'America/Sao_Paulo',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast",
"id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"name": "Black Friday: acesso antecipado",
"status": "scheduled",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"segment": {
"id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"name": "Clientes ativos"
},
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"topic": {
"id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"name": "Novidades"
},
"template_id": null,
"from": "Loja Exemplo <[email protected]>",
"reply_to": ["[email protected]"],
"detect_replies": false,
"subject": "{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você",
"preheader": "Até 40% de desconto a partir de quinta, só para clientes.",
"language": null,
"html": "<p>Oi, {{{FIRST_NAME|cliente}}}!</p><p>Separamos descontos de até 40% para quem é do plano {{{contact.plan}}}.</p><p><a href=\"https://lojaexemplo.com.br/black-friday\">Ver as ofertas</a></p><p><a href=\"{{{POSTAL_UNSUBSCRIBE_URL}}}\">Não quero mais receber</a></p>",
"text": null,
"variables": {},
"editor": null,
"scheduled_at": "2026-10-28T12:00:00.000000Z",
"schedule_timezone": "America/Sao_Paulo",
"queued_at": null,
"sent_at": null,
"canceled_at": null,
"recipients_count": 0,
"processed_count": 0,
"stats": {
"sent": 0,
"delivered": 0,
"opened": 0,
"clicked": 0,
"bounced": 0,
"complained": 0,
"unsubscribed": 0,
"failed": 0
},
"error": null,
"created_at": "2026-10-21T18:40:00.000000Z",
"updated_at": "2026-10-24T16:10:00.000000Z"
}
#Cancelar o envio
POST/broadcasts/{id}/cancel
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órioID da campanha.
Erros
- 409
invalid_broadcast_statusA campanha é um rascunho ou já terminou. - 404
not_foundO recurso não existe ou é de outro time.
cURL
curl -X POST https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/cancel \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/cancel', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/cancel",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/cancel', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast",
"id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"name": "Black Friday: acesso antecipado",
"status": "canceled",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"segment": {
"id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"name": "Clientes ativos"
},
"topic_id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"topic": {
"id": "019a1d95-4c5d-7e6f-8a7b-8c9d0e1f2a3b",
"name": "Novidades"
},
"template_id": null,
"from": "Loja Exemplo <[email protected]>",
"reply_to": ["[email protected]"],
"detect_replies": false,
"subject": "{{{FIRST_NAME|Cliente}}}, a Black Friday começa mais cedo para você",
"preheader": "Até 40% de desconto a partir de quinta, só para clientes.",
"language": null,
"html": "<p>Oi, {{{FIRST_NAME|cliente}}}!</p><p>Separamos descontos de até 40% para quem é do plano {{{contact.plan}}}.</p><p><a href=\"https://lojaexemplo.com.br/black-friday\">Ver as ofertas</a></p><p><a href=\"{{{POSTAL_UNSUBSCRIBE_URL}}}\">Não quero mais receber</a></p>",
"text": null,
"variables": {},
"editor": null,
"scheduled_at": null,
"schedule_timezone": null,
"queued_at": "2026-10-22T12:00:00.000000Z",
"sent_at": null,
"canceled_at": "2026-10-22T12:02:10.000000Z",
"recipients_count": 1247,
"processed_count": 500,
"stats": {
"sent": 488,
"delivered": 471,
"opened": 96,
"clicked": 21,
"bounced": 2,
"complained": 0,
"unsubscribed": 1,
"failed": 0
},
"error": null,
"created_at": "2026-10-21T18:40:00.000000Z",
"updated_at": "2026-10-22T12:02:10.000000Z"
}
#Duplicar
POST/broadcasts/{id}/duplicate
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órioID da campanha.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
cURL
curl -X POST https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/duplicate \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/duplicate', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/duplicate",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/duplicate', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "broadcast",
"id": "019a1f05-3d4e-7f50-9b6c-7d8e9f0a1b2c"
}
#Listar destinatários
GET/broadcasts/{id}/recipients
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órioID da campanha.
Parâmetros de consulta
limitintegerDe 1 a 100. Padrão: 20.afterstringCursor da próxima página.beforestringCursor da página anterior.statusstringUm status de e-mail, ouunsubscribedpara quem se descadastrou.searchstringBusca no endereço.
Erros
- 422
validation_errorStatus desconhecido. - 404
not_foundO recurso não existe ou é de outro time.
cURL
curl 'https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/recipients?status=clicked' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/recipients?status=clicked', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/recipients",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"status": "clicked",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('broadcasts/019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b/recipients', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'status' => 'clicked',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "list",
"has_more": true,
"data": [
{
"object": "broadcast_recipient",
"id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
"email_id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
"contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"email": "[email protected]",
"first_name": "Ana",
"last_name": "Souza",
"status": "clicked",
"unsubscribed": false,
"error": null,
"sent_at": "2026-10-22T12:00:04.000000Z",
"created_at": "2026-10-22T12:00:02.000000Z"
}
]
}
#Métricas 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:
GET /emails/metrics?dimensions=broadcast&metrics=delivered,open_rate,click_rate,unsubscribe_rate&start_date=2026-10-01
{
"object": "metrics",
"start_date": "2026-10-01T00:00:00.000Z",
"end_date": "2026-10-24T16:10:00.000Z",
"metrics": [
"delivered",
"open_rate",
"click_rate",
"unsubscribe_rate"
],
"dimensions": ["broadcast"],
"granularity": "daily",
"totals": {
"delivered": 1236,
"open_rate": 0.4951,
"click_rate": 0.1521,
"unsubscribe_rate": 0.0073
},
"data": [
{
"broadcast_id": "019a1f00-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
"broadcast_name": "Black Friday: acesso antecipado",
"delivered": 1236,
"open_rate": 0.4951,
"click_rate": 0.1521,
"unsubscribe_rate": 0.0073
}
]
}
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).