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.
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. | 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. | 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.” O texto ganha uma linha com o link. - 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.
Detalhes e boas práticas no guia Marketing por e-mail.
#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]"],
"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
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.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.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.",
"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);
{
"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.
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]"],
"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}
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.
Parâmetros de consulta
segment_idstringobrigatórioID do segmento.topic_idstringID do tópico.
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
}
#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.
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 [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ó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. - 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.
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.
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. - 403
sending_pausedOs envios do time estão pausados. - 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"
}'
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);
{
"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
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]"],
"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
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ó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).