Referência da API

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

Acesso total ou só envioAceita Idempotency-Key

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-Keystring
    Chave de 1 a 256 caracteres para repetir a requisição sem duplicar o envio. Veja Idempotência.

Corpo da requisição

  • fromstringobrigatório
    Remetente, no formato [email protected] ou Nome <[email protected]>. O domínio precisa estar verificado e com envio ativo. Até 998 caracteres.
  • tostring | string[]obrigatório
    Destinatários. Uma string ou uma lista de até 50 endereços.
  • subjectstringobrigatório
    Assunto, com até 998 caracteres.
  • htmlstring
    Corpo em HTML. Obrigatório quando não houver text.
  • textstring
    Versão em texto puro. Recomendada mesmo com html: melhora a entregabilidade.
  • ccstring | string[]
    Cópia.
  • bccstring | string[]
    Cópia oculta.
  • reply_tostring | string[]
    Endereços para resposta (Reply-To).
  • headersobject
    Cabeçalhos extras, como {"List-Unsubscribe": "<https://…>"}. Valores com até 998 caracteres e sem quebra de linha. Não é possível sobrescrever From, To, Cc, Bcc, Subject, Reply-To, Sender, Date, Message-ID, Return-Path, MIME-Version, Content-Type, Content-Transfer-Encoding nem DKIM-Signature.
  • tagsobject[]
    Até 50 etiquetas para filtrar e identificar o envio nos webhooks.
    • namestringobrigatório
      Letras ASCII, números, _ ou -, até 256 caracteres. Não pode começar com postal_.
    • valuestring
      Mesmas regras do nome. Opcional.
  • attachmentsobject[]
    Anexos, com até 40 MB no total. Não aceito em lote.
    • contentstring
      Conteúdo do arquivo em Base64. Obrigatório sem path.
    • pathstring
      URL http(s) pública de onde a Postal baixa o arquivo no momento do envio. Obrigatório sem content.
    • filenamestring
      Nome do arquivo (até 255 caracteres). Com path, o padrão é o nome no fim da URL.
    • content_typestring
      Tipo MIME, como application/pdf. Sem ele, o tipo é deduzido pela extensão.
    • content_idstring
      Para imagens embutidas: use <img src="cid:logo"> no HTML e "content_id": "logo" aqui.
  • scheduled_atstring
    Data 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.
  • templateobject
    Envia com um template publicado em vez de html/text. Veja Envio com template.
    • idstringobrigatório
      ID ou alias do template.
    • variablesobject
      Valores 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.

Corpo 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:…".

Corpo com anexos
{
  "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

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 403validation_error
    O domínio do from não está verificado ou não é do seu time.
  • 403restricted_api_key
    A chave só pode enviar de outro domínio.
  • 403sending_paused
    Os envios do time estão pausados por taxa alta de bounces ou reclamações.
  • 404not_found
    O template não existe ou não foi publicado.
  • 409invalid_idempotent_request
    A mesma Idempotency-Key foi usada com outro corpo.
  • 429daily_quota_exceeded
    A cota diária do plano acabou. Ela volta às 00:00 UTC.
  • 429monthly_quota_exceeded
    A cota mensal do plano acabou.
POST /emails

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);
200Resposta
{
  "id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b"
}

#Enviar em lote

POST/emails/batch

Acesso total ou só envioAceita Idempotency-Key

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ório
    De 1 a 100 e-mails, cada um com from, to, subject, html ou text e, se quiser, cc, bcc, reply_to, headers, tags e template.

Erros

  • 422validation_error
    O corpo não é uma lista, tem mais de 100 itens, ou algum item é inválido (com a posição na mensagem).
  • 403validation_error
    Algum from usa um domínio não verificado.
  • 429daily_quota_exceeded
    O lote passaria da cota diária ou mensal.
POST /emails/batch

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);
200Resposta
{
  "data": [
    {
      "id": "019a1b2c-51aa-7c21-9f02-6b7c8d9e0f1a"
    },
    {
      "id": "019a1b2c-51ab-7d30-8e11-2a3b4c5d6e7f"
    }
  ]
}

#Listar e-mails

GET/emails

Chave de acesso total

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

  • limitinteger
    Itens por página, de 1 a 100. Padrão: 20.
  • afterstring
    ID do último item recebido, para a próxima página.
  • beforestring
    ID do primeiro item recebido, para a página anterior.
  • searchstring
    Busca em remetente, assunto e destinatários. Um ID completo encontra o e-mail exato.
  • statusstring
    Um ou mais status separados por vírgula, como bounced,complained. Veja os status.
  • domain_idstring
    Só e-mails de um domínio.
  • api_key_idstring
    Só e-mails enviados com uma chave.
  • start_datestring
    Criados a partir desta data (ISO 8601).
  • end_datestring
    Criados até esta data (ISO 8601).

Erros

  • 422validation_error
    Filtro ou cursor inválido, ou after e before juntos.
GET /emails

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);
200Resposta
{
  "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}

Chave de acesso total

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ório
    ID do e-mail.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
GET /emails/{id}

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);
200Resposta
{
  "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}

Chave de acesso total

Muda a data de envio de um e-mail agendado. Só funciona enquanto o status for scheduled.

Parâmetros de caminho

  • idstringobrigatório
    ID do e-mail.

Corpo da requisição

  • scheduled_atstringobrigatório
    Nova data em ISO 8601, no futuro e a no máximo 30 dias.

Erros

  • 422validation_error
    O e-mail não está agendado ou a data é inválida.
  • 404not_found
    O recurso não existe ou é de outro time.
PATCH /emails/{id}

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);
200Resposta
{
  "object": "email",
  "id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b"
}

#Cancelar um e-mail agendado

POST/emails/{id}/cancel

Chave de acesso total

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ório
    ID do e-mail.

Erros

  • 422validation_error
    O e-mail não está agendado (já saiu ou já foi cancelado).
  • 404not_found
    O recurso não existe ou é de outro time.
POST /emails/{id}/cancel

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);
200Resposta
{
  "object": "email",
  "id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b"
}

#Mensagem original

GET/emails/{id}/raw

Chave de acesso total

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ório
    ID do e-mail.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
GET /emails/{id}/raw

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();
200Resposta (text/plain)
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

Chave de acesso total

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ório
    ID do e-mail.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
GET /emails/{id}/insights

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);
200Resposta
{
  "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

Chave de acesso total

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ório
    ID do e-mail.

Corpo da requisição

  • expires_instring
    Validade, de 1 minuto a 48 horas: 10m, 2 hours, 1 day… Padrão: 48h.

Erros

  • 422validation_error
    expires_in fora do formato ou do intervalo.
  • 404not_found
    O recurso não existe ou é de outro time.
POST /emails/{id}/share

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);
200Resposta
{
  "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

Chave de acesso total

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ório
    ID do e-mail.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
GET /emails/{id}/attachments

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);
200Resposta
{
  "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}

Chave de acesso total

Devolve um anexo com o link de download.

Parâmetros de caminho

  • idstringobrigatório
    ID do e-mail.
  • attachment_idstringobrigatório
    ID do anexo.

Erros

  • 404not_found
    O recurso não existe ou é de outro time.
GET /emails/{id}/attachments/{attachment_id}

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);
200Resposta
{
  "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.

StatusSignificado
queuedNa fila, saindo em instantes.
scheduledAgendado para scheduled_at.
sentAceito pelo provedor de envio.
delivery_delayedO servidor do destinatário adiou a entrega; novas tentativas acontecem sozinhas.
deliveredEntregue ao servidor do destinatário.
openedAberto (com rastreamento de aberturas ligado no domínio).
clickedUm link foi clicado (com rastreamento de cliques ligado).
bouncedDevolvido. Bounces permanentes vão para a lista de supressão.
complainedO destinatário marcou como spam. O endereço vai para a lista de supressão.
failedNão foi possível enviar (domínio não verificado, envio pausado, anexo inacessível…).
suppressedTodos ou parte dos destinatários estavam na lista de supressão.
canceledAgendamento cancelado.