E-mails
Enviar, listar, agendar e cancelar e-mails, com anexos, tags, cabeçalhos e templates.
Envie e-mails, acompanhe o status de cada mensagem, agende, cancele e consulte anexos. Os envios pela API e pelo SMTP aparecem nos mesmos endpoints.
#Enviar um e-mail
POST/emails
Coloca um e-mail na fila de envio e devolve o ID. O envio acontece em segundos; acompanhe o resultado com GET /emails/{id} ou com webhooks. Antes de enviar, a Postal remove os destinatários que estão na lista de supressão.
Cabeçalhos
Idempotency-KeystringChave de 1 a 256 caracteres para repetir a requisição sem duplicar o envio. Veja Idempotência.
Corpo da requisição
fromstringobrigatórioRemetente, no formato[email protected]ouNome <[email protected]>. O domínio precisa estar verificado e com envio ativo. Até 998 caracteres.tostring | string[]obrigatórioDestinatários. Uma string ou uma lista de até 50 endereços.subjectstringobrigatórioAssunto, com até 998 caracteres.htmlstringCorpo em HTML. Obrigatório quando não houvertext.textstringVersão em texto puro. Recomendada mesmo comhtml: melhora a entregabilidade.ccstring | string[]Cópia.bccstring | string[]Cópia oculta.reply_tostring | string[]Endereços para resposta (Reply-To).headersobjectCabeçalhos extras, como{"List-Unsubscribe": "<https://…>"}. Valores com até 998 caracteres e sem quebra de linha. Não é possível sobrescreverFrom,To,Cc,Bcc,Subject,Reply-To,Sender,Date,Message-ID,Return-Path,MIME-Version,Content-Type,Content-Transfer-EncodingnemDKIM-Signature.tagsobject[]Até 50 etiquetas para filtrar e identificar o envio nos webhooks.namestringobrigatórioLetras ASCII, números,_ou-, até 256 caracteres. Não pode começar compostal_.valuestringMesmas regras do nome. Opcional.
attachmentsobject[]Anexos, com até 40 MB no total. Não aceito em lote.contentstringConteúdo do arquivo em Base64. Obrigatório sempath.pathstringURL http(s) pública de onde a Postal baixa o arquivo no momento do envio. Obrigatório semcontent.filenamestringNome do arquivo (até 255 caracteres). Compath, o padrão é o nome no fim da URL.content_typestringTipo MIME, comoapplication/pdf. Sem ele, o tipo é deduzido pela extensão.content_idstringPara imagens embutidas: use<img src="cid:logo">no HTML e"content_id": "logo"aqui.
scheduled_atstringData e hora do envio em ISO 8601 (ex.:2026-10-25T09:00:00-03:00). Precisa estar no futuro e a no máximo 30 dias. Não aceito em lote.templateobjectidstringobrigatórioID ou alias do template.variablesobjectValores das variáveis, como{"NAME": "Ana"}.
Envio com template
Com template, a Postal usa a última versão publicada: from, subject e reply_to do template valem quando não vierem no corpo, e as variáveis {{{NAME}}} são preenchidas com variables (no HTML, os valores são escapados). Uma variável sem valor e sem valor padrão gera 422. Não envie html ou text junto com template.
{
"from": "Loja Exemplo <[email protected]>",
"to": "[email protected]",
"template": {
"id": "order-shipped",
"variables": {
"NAME": "Ana",
"ORDER": "10482"
}
}
}
Anexos e imagens embutidas
Mande o arquivo em Base64 em content ou uma URL em path. Para uma imagem dentro do HTML, use o mesmo content_id no atributo src="cid:…".
{
"from": "Financeiro <[email protected]>",
"to": ["[email protected]"],
"subject": "Sua nota fiscal",
"html": "<img src=\"cid:logo\" alt=\"Loja Exemplo\"><p>Segue a nota fiscal do pedido #10482.</p>",
"attachments": [
{
"filename": "invoice-10482.pdf",
"content": "JVBERi0xLjcKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFI+PgpzdHJlYW0K…"
},
{
"path": "https://cdn.lojaexemplo.com.br/logo.png",
"content_id": "logo"
}
]
}
Agendamento
Com scheduled_at, o e-mail fica com status scheduled até a hora marcada. Dá para reagendar ou cancelar enquanto ele não sair. A cota é contada quando o e-mail é criado.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual. - 403
validation_errorO domínio dofromnão está verificado ou não é do seu time. - 403
restricted_api_keyA chave só pode enviar de outro domínio. - 403
sending_pausedOs envios do time estão pausados por taxa alta de bounces ou reclamações. - 404
not_foundOtemplatenão existe ou não foi publicado. - 409
invalid_idempotent_requestA mesmaIdempotency-Keyfoi usada com outro corpo. - 429
daily_quota_exceededA cota diária do plano acabou. Ela volta às 00:00 UTC. - 429
monthly_quota_exceededA cota mensal do plano acabou.
cURL
curl -X POST https://api.postal.so/emails \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Idempotency-Key: order-10482-shipped" \
-H "Content-Type: application/json" \
-d '{
"from": "Loja Exemplo <[email protected]>",
"to": ["[email protected]"],
"subject": "Seu pedido #10482 saiu para entrega",
"html": "<p>O pedido <b>#10482</b> está a caminho.</p>",
"text": "O pedido #10482 está a caminho.",
"tags": [
{
"name": "category",
"value": "shipping"
}
]
}'
Node.js
const response = await fetch('https://api.postal.so/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'order-10482-shipped',
},
body: JSON.stringify({
from: 'Loja Exemplo <[email protected]>',
to: ['[email protected]'],
subject: 'Seu pedido #10482 saiu para entrega',
html: '<p>O pedido <b>#10482</b> está a caminho.</p>',
text: 'O pedido #10482 está a caminho.',
tags: [
{
name: 'category',
value: 'shipping',
},
],
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/emails",
headers={
"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}",
"Idempotency-Key": "order-10482-shipped",
},
json={
"from": "Loja Exemplo <[email protected]>",
"to": ["[email protected]"],
"subject": "Seu pedido #10482 saiu para entrega",
"html": "<p>O pedido <b>#10482</b> está a caminho.</p>",
"text": "O pedido #10482 está a caminho.",
"tags": [
{
"name": "category",
"value": "shipping",
},
],
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('emails', [
'headers' => [
'Authorization' => 'Bearer '.getenv('POSTAL_API_KEY'),
'Idempotency-Key' => 'order-10482-shipped',
],
'json' => [
'from' => 'Loja Exemplo <[email protected]>',
'to' => ['[email protected]'],
'subject' => 'Seu pedido #10482 saiu para entrega',
'html' => '<p>O pedido <b>#10482</b> está a caminho.</p>',
'text' => 'O pedido #10482 está a caminho.',
'tags' => [
[
'name' => 'category',
'value' => 'shipping',
],
],
],
]);
$data = json_decode($response->getBody(), true);
{
"id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b"
}
#Enviar em lote
POST/emails/batch
Envia até 100 e-mails numa requisição. O corpo é uma lista de objetos com os mesmos campos de POST /emails, exceto attachments e scheduled_at. Tudo é validado antes: ou todos os e-mails entram na fila, ou nenhum. Os erros de validação indicam a posição, como [3] Missing `subject` field., e a cota é verificada para o lote inteiro.
Corpo da requisição (lista)
[ ]object[]obrigatórioDe 1 a 100 e-mails, cada um comfrom,to,subject,htmloutexte, se quiser,cc,bcc,reply_to,headers,tagsetemplate.
Erros
- 422
validation_errorO corpo não é uma lista, tem mais de 100 itens, ou algum item é inválido (com a posição na mensagem). - 403
validation_errorAlgumfromusa um domínio não verificado. - 429
daily_quota_exceededO lote passaria da cota diária ou mensal.
cURL
curl -X POST https://api.postal.so/emails/batch \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"from": "Loja Exemplo <[email protected]>",
"to": ["[email protected]"],
"subject": "Seu cupom chegou",
"html": "<p>Olá, Ana! Use o cupom <b>BEMVINDA</b>.</p>"
},
{
"from": "Loja Exemplo <[email protected]>",
"to": ["[email protected]"],
"subject": "Seu cupom chegou",
"html": "<p>Olá, Bruno! Use o cupom <b>BEMVINDO</b>.</p>"
}
]'
Node.js
const response = await fetch('https://api.postal.so/emails/batch', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify([
{
from: 'Loja Exemplo <[email protected]>',
to: ['[email protected]'],
subject: 'Seu cupom chegou',
html: '<p>Olá, Ana! Use o cupom <b>BEMVINDA</b>.</p>',
},
{
from: 'Loja Exemplo <[email protected]>',
to: ['[email protected]'],
subject: 'Seu cupom chegou',
html: '<p>Olá, Bruno! Use o cupom <b>BEMVINDO</b>.</p>',
},
]),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/emails/batch",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json=[
{
"from": "Loja Exemplo <[email protected]>",
"to": ["[email protected]"],
"subject": "Seu cupom chegou",
"html": "<p>Olá, Ana! Use o cupom <b>BEMVINDA</b>.</p>",
},
{
"from": "Loja Exemplo <[email protected]>",
"to": ["[email protected]"],
"subject": "Seu cupom chegou",
"html": "<p>Olá, Bruno! Use o cupom <b>BEMVINDO</b>.</p>",
},
],
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('emails/batch', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
[
'from' => 'Loja Exemplo <[email protected]>',
'to' => ['[email protected]'],
'subject' => 'Seu cupom chegou',
'html' => '<p>Olá, Ana! Use o cupom <b>BEMVINDA</b>.</p>',
],
[
'from' => 'Loja Exemplo <[email protected]>',
'to' => ['[email protected]'],
'subject' => 'Seu cupom chegou',
'html' => '<p>Olá, Bruno! Use o cupom <b>BEMVINDO</b>.</p>',
],
],
]);
$data = json_decode($response->getBody(), true);
{
"data": [
{
"id": "019a1b2c-51aa-7c21-9f02-6b7c8d9e0f1a"
},
{
"id": "019a1b2c-51ab-7d30-8e11-2a3b4c5d6e7f"
}
]
}
#Listar e-mails
GET/emails
Lista os e-mails enviados, do mais recente para o mais antigo, com paginação por cursor. A lista não traz os eventos; use GET /emails/{id} para ver o histórico.
Parâmetros de consulta
limitintegerItens por página, de 1 a 100. Padrão: 20.afterstringID do último item recebido, para a próxima página.beforestringID do primeiro item recebido, para a página anterior.searchstringBusca em remetente, assunto e destinatários. Um ID completo encontra o e-mail exato.statusstringUm ou mais status separados por vírgula, comobounced,complained. Veja os status.domain_idstringSó e-mails de um domínio.api_key_idstringSó e-mails enviados com uma chave.start_datestringCriados a partir desta data (ISO 8601).end_datestringCriados até esta data (ISO 8601).
Erros
- 422
validation_errorFiltro ou cursor inválido, ouafterebeforejuntos.
cURL
curl 'https://api.postal.so/emails?limit=2&status=delivered,opened' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/emails?limit=2&status=delivered,opened', {
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/emails",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"limit": 2,
"status": "delivered,opened",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('emails', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'limit' => 2,
'status' => 'delivered,opened',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "list",
"has_more": true,
"data": [
{
"object": "email",
"id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
"message_id": "<0103019a1b2c4d5e-6f7a8b9c-1d2e-4f3a-9b8c-7d6e5f4a3b2c-000000@postal.so>",
"to": ["[email protected]"],
"from": "Loja Exemplo <[email protected]>",
"created_at": "2026-10-24T13:02:11.000000Z",
"subject": "Seu pedido #10482 saiu para entrega",
"html": "<p>O pedido <b>#10482</b> está a caminho.</p>",
"text": "O pedido #10482 está a caminho.",
"bcc": [],
"cc": [],
"reply_to": ["[email protected]"],
"last_event": "delivered",
"scheduled_at": null,
"tags": [
{
"name": "category",
"value": "shipping"
}
]
}
]
}
#Obter um e-mail
GET/emails/{id}
Devolve o e-mail com o conteúdo, o status atual (last_event) e a lista de eventos. Também traz de onde ele veio: source é api ou smtp, e log_id aponta para o log da requisição.
Parâmetros de caminho
idstringobrigatórioID do e-mail.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
cURL
curl https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b', {
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/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
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('emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "email",
"id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
"message_id": "<0103019a1b2c4d5e-6f7a8b9c-1d2e-4f3a-9b8c-7d6e5f4a3b2c-000000@postal.so>",
"to": ["[email protected]"],
"from": "Loja Exemplo <[email protected]>",
"created_at": "2026-10-24T13:02:11.000000Z",
"subject": "Seu pedido #10482 saiu para entrega",
"html": "<p>O pedido <b>#10482</b> está a caminho.</p>",
"text": "O pedido #10482 está a caminho.",
"bcc": [],
"cc": [],
"reply_to": ["[email protected]"],
"last_event": "delivered",
"scheduled_at": null,
"tags": [
{
"name": "category",
"value": "shipping"
}
],
"events": [
{
"type": "email.sent",
"occurred_at": "2026-10-24T13:02:12.000000Z",
"data": null
},
{
"type": "email.delivered",
"occurred_at": "2026-10-24T13:02:14.000000Z",
"data": {
"recipients": ["[email protected]"]
}
}
],
"domain_id": "0199f0e1-2a3b-7c4d-8e5f-6a7b8c9d0e1f",
"api_key_id": "0199f0e2-7b8c-7d9e-8f0a-1b2c3d4e5f6a",
"template_id": null,
"source": "api",
"log_id": "019a1b2c-4d60-7a11-b2c3-d4e5f6a7b8c9"
}
#Reagendar um e-mail
PATCH/emails/{id}
Muda a data de envio de um e-mail agendado. Só funciona enquanto o status for scheduled.
Parâmetros de caminho
idstringobrigatórioID do e-mail.
Corpo da requisição
scheduled_atstringobrigatórioNova data em ISO 8601, no futuro e a no máximo 30 dias.
Erros
- 422
validation_errorO e-mail não está agendado ou a data é inválida. - 404
not_foundO recurso não existe ou é de outro time.
cURL
curl -X PATCH https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scheduled_at": "2026-10-26T09:00:00-03:00"
}'
Node.js
const response = await fetch('https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
scheduled_at: '2026-10-26T09:00:00-03:00',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.patch(
"https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"scheduled_at": "2026-10-26T09:00:00-03:00",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->patch('emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'scheduled_at' => '2026-10-26T09:00:00-03:00',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "email",
"id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b"
}
#Cancelar um e-mail agendado
POST/emails/{id}/cancel
Cancela o envio de um e-mail agendado. O status passa a canceled e um evento email.canceled entra no histórico.
Parâmetros de caminho
idstringobrigatórioID do e-mail.
Erros
- 422
validation_errorO e-mail não está agendado (já saiu ou já foi cancelado). - 404
not_foundO recurso não existe ou é de outro time.
cURL
curl -X POST https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/cancel \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/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/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/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('emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/cancel', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "email",
"id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b"
}
#Mensagem original
GET/emails/{id}/raw
Devolve a mensagem como foi enviada, no formato RFC 5322 (text/plain): a original, para envios por SMTP, ou reconstruída, para envios pela API.
Parâmetros de caminho
idstringobrigatórioID do e-mail.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
cURL
curl https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/raw \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/raw', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const message = await response.text();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/raw",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
message = response.text
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/raw', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$message = (string) $response->getBody();
From: Loja Exemplo <[email protected]>
To: [email protected]
Subject: Seu pedido #10482 saiu para entrega
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="b1_Xk2p"
--b1_Xk2p
Content-Type: text/plain; charset=utf-8
O pedido #10482 está a caminho.
--b1_Xk2p--
#Diagnóstico de entregabilidade
GET/emails/{id}/insights
Roda verificações de boas práticas no e-mail e diz se cada uma passou. Os códigos são: has_text_version, no_link_shorteners, https_links, no_ip_links, images_have_alt, html_under_gmail_limit (HTML até 102 KB), enough_text, reply_friendly_sender, subject_not_all_caps, unsubscribe_option e dmarc_configured (só quando o DNS responde). A IA explica como corrigir os que falharam.
Parâmetros de caminho
idstringobrigatórioID do e-mail.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
cURL
curl https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/insights \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/insights', {
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/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/insights",
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('emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/insights', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "list",
"data": [
{
"code": "has_text_version",
"passed": true,
"data": {}
},
{
"code": "https_links",
"passed": false,
"data": {
"count": 1
}
},
{
"code": "dmarc_configured",
"passed": true,
"data": {
"domain": "lojaexemplo.com.br"
}
}
]
}
#Criar link de visualização
POST/emails/{id}/share
Cria um link público e temporário para ver o e-mail no navegador, útil para mostrar a alguém sem acesso ao painel. O HTML abre isolado, sem scripts.
Parâmetros de caminho
idstringobrigatórioID do e-mail.
Corpo da requisição
expires_instringValidade, de 1 minuto a 48 horas:10m,2 hours,1 day… Padrão:48h.
Erros
- 422
validation_errorexpires_infora do formato ou do intervalo. - 404
not_foundO recurso não existe ou é de outro time.
cURL
curl -X POST https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/share \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expires_in": "2h"
}'
Node.js
const response = await fetch('https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/share', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
expires_in: '2h',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/share",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"expires_in": "2h",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/share', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'expires_in' => '2h',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "email",
"id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
"url": "https://api.postal.so/shared/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b?expires=1761321731&signature=4f1c…"
}
#Listar anexos
GET/emails/{id}/attachments
Lista os anexos de um e-mail enviado. Cada um traz um download_url assinado, válido por 1 hora. Para anexos enviados por path, o link é a URL original e expires_at vem nulo. Aceita limit, after e before, como as outras listas.
Parâmetros de caminho
idstringobrigatórioID do e-mail.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
cURL
curl https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/attachments \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/attachments', {
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/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/attachments",
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('emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/attachments', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "list",
"has_more": false,
"data": [
{
"id": "019a1b2c-4d5f-7e70-9b8a-7c6d5e4f3a2b",
"filename": "invoice-10482.pdf",
"content_type": "application/pdf",
"content_id": null,
"content_disposition": "attachment",
"download_url": "https://api.postal.so/attachments/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/019a1b2c-4d5f-7e70-9b8a-7c6d5e4f3a2b?expires=1761314531&signature=9a7e…",
"expires_at": "2026-10-24T14:02:11.000Z",
"size": 48213
}
]
}
#Obter um anexo
GET/emails/{id}/attachments/{attachment_id}
Devolve um anexo com o link de download.
Parâmetros de caminho
idstringobrigatórioID do e-mail.attachment_idstringobrigatórioID do anexo.
Erros
- 404
not_foundO recurso não existe ou é de outro time.
cURL
curl https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/attachments/019a1b2c-4d5f-7e70-9b8a-7c6d5e4f3a2b \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/attachments/019a1b2c-4d5f-7e70-9b8a-7c6d5e4f3a2b', {
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/emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/attachments/019a1b2c-4d5f-7e70-9b8a-7c6d5e4f3a2b",
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('emails/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/attachments/019a1b2c-4d5f-7e70-9b8a-7c6d5e4f3a2b', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "attachment",
"id": "019a1b2c-4d5f-7e70-9b8a-7c6d5e4f3a2b",
"filename": "invoice-10482.pdf",
"content_type": "application/pdf",
"content_id": null,
"content_disposition": "attachment",
"download_url": "https://api.postal.so/attachments/019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b/019a1b2c-4d5f-7e70-9b8a-7c6d5e4f3a2b?expires=1761314531&signature=9a7e…",
"expires_at": "2026-10-24T14:02:11.000Z",
"size": 48213
}
#Status de um e-mail
O campo last_event mostra o estado mais avançado do e-mail. Como os eventos podem chegar fora de ordem, o status só avança: um e-mail clicked não volta para delivered.
| Status | Significado |
|---|---|
queued | Na fila, saindo em instantes. |
scheduled | Agendado para scheduled_at. |
sent | Aceito pelo provedor de envio. |
delivery_delayed | O servidor do destinatário adiou a entrega; novas tentativas acontecem sozinhas. |
delivered | Entregue ao servidor do destinatário. |
opened | Aberto (com rastreamento de aberturas ligado no domínio). |
clicked | Um link foi clicado (com rastreamento de cliques ligado). |
bounced | Devolvido. Bounces permanentes vão para a lista de supressão. |
complained | O destinatário marcou como spam. O endereço vai para a lista de supressão. |
failed | Não foi possível enviar (domínio não verificado, envio pausado, anexo inacessível…). |
suppressed | Todos ou parte dos destinatários estavam na lista de supressão. |
canceled | Agendamento cancelado. |