Entrar Criar conta
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.

Quem recebe um e-mail enviado por aqui (em to e cc) é guardado na audiência como contato não confirmado, que não recebe campanhas nem automações até você confirmar. Dá para desligar isso em Configurações › Time.

#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. Para testar sem enviar de verdade, use os endereços de teste.

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. Pode ser omitido quando o destinatário é um contato: veja user_id, external_id e contact_id.
  • user_idstring
    Alternativa a to: o ID da pessoa no seu sistema, o external_id de um contato (user_id é um apelido). O e-mail vai para o endereço desse contato. Ignorado quando to é enviado.
  • external_idstring
    O mesmo que user_id. Se os dois forem enviados, vale o user_id.
  • contact_idstring
    Alternativa a to: o ID do contato na Postal (ou o endereço de e-mail dele). Vale depois de user_id e external_id.
  • 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. Para enviar um código de uso único que o cliente de e-mail possa preencher, use OTP-Token: veja Cabeçalho OTP-Token. Quando o OTP-Token vem na requisição, a API confere o formato e responde 422 se o valor for inválido ou se o cabeçalho aparecer mais de uma vez, em qualquer caixa de letras (o que a Postal valida).
  • 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. O plano pode permitir menos que isso (ou nenhum anexo): acima do limite do plano, a resposta é 403 plan_limit_reached. Um path que a Postal não consegue baixar na hora do envio deixa o e-mail com status failed.
    • 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. Depende do recurso de agendamento do plano (403 plan_limit_reached sem ele).
  • 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, e o mesmo vale para um valor que não seja texto nem número ou, numa variável do tipo number, que não seja numérico. Valores de variáveis que o template não declara também são preenchidos. Não envie html ou text junto com template. Um template pausado pelo plano responde 403 paused_by_plan.

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"
    }
  ]
}

Enviar para um contato

Se você guarda as pessoas como contatos, não precisa repetir o endereço: mande o user_id (o external_id do contato) no lugar de to e a Postal usa o e-mail dele. Vale também no lote. Um contato que não existe devolve 404. A lista de supressão continua valendo: se o endereço estiver nela, o e-mail não sai.

Corpo com um contato
{
  "from": "Loja Exemplo <[email protected]>",
  "user_id": "user_42",
  "template": {
    "id": "order-shipped",
    "variables": {
      "NAME": "Ana",
      "ORDER": "10482"
    }
  }
}

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, e um evento email.scheduled entra no histórico.

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.
  • 403paused_by_plan
    O domínio do from, o template ou a própria chave está pausado porque passa do limite do plano. Veja Limites do plano.
  • 403plan_limit_reached
    O plano não inclui o que o envio pede: scheduled_at, endereços de teste, ou anexos (ou o total passa do limite do plano).
  • 404not_found
    O template não existe ou não foi publicado.
  • 404not_found
    Nenhum contato tem o user_id, o external_id ou o contact_id informado.
  • 400invalid_idempotency_key
    A Idempotency-Key está vazia ou tem mais de 256 caracteres.
  • 409concurrent_idempotent_requests
    Outra requisição com a mesma Idempotency-Key ainda está em andamento.
  • 409invalid_idempotent_request
    A mesma Idempotency-Key foi usada com outro corpo.
  • 429daily_quota_exceeded
    A cota diária acabou: o limite diário do plano ou o teto da reputação da conta, o que for menor. Ela volta às 00:00 UTC. Quando é a reputação que limita, a mensagem diz o nível e o teto.
  • 429monthly_quota_exceeded
    A cota mensal do plano acabou, e a reserva do Adicional de envios também, se o time tem uma. A mensagem diz o que fazer, inclusive onde comprar o adicional quando o plano o oferece.
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 (os itens só para endereços de teste não contam). O envio em lote é um recurso do plano.

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. Em vez de to, vale user_id, external_id ou contact_id, como em POST /emails.

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.
  • 403plan_limit_reached
    O plano não inclui o envio em lote (ou os endereços de teste, se algum item os usa).
  • 403sending_paused
    Os envios do time estão pausados por taxa alta de bounces ou reclamações.
  • 403paused_by_plan
    Algum from usa um domínio pausado pelo plano, ou algum item usa um template pausado.
  • 404not_found
    Algum item usa um template inexistente ou não publicado, ou um contato que não existe (a mensagem traz a posição).
  • 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. Os destinatários incluem cc e bcc; a busca tem até 200 caracteres.
  • statusstring
    Um ou mais status separados por vírgula, como bounced,complained. Veja os status. Um valor que não é status devolve 422.
  • 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). Uma data sem hora, como 2026-10-01, começa à meia-noite UTC.
  • end_datestring
    Criados até esta data (ISO 8601). Uma data sem hora, como 2026-10-24, vale até o fim desse dia (UTC). Se start_date for depois de end_date, a resposta é 422.

Erros

  • 422validation_error
    Filtro ou cursor inválido, ou after e before juntos. Também quando start_date é depois de end_date.
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@sa-east-1.amazonses.com>",
      "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,
      "simulated": false,
      "tags": [
        {
          "name": "category",
          "value": "shipping"
        }
      ]
    }
  ]
}

#Obter um e-mail

GET/emails/{id}

Acesso total ou só envio

Devolve o e-mail com o conteúdo, o status atual (last_event) e a lista de eventos. Nos e-mails para os endereços de teste, simulated é true. Também traz de onde ele veio: source é api ou smtp, e log_id aponta para o log da requisição. Os campos estão em O objeto e-mail e os tipos de evento em Eventos de um e-mail.

Quais chaves podem chamar: uma chave de acesso total (ou o painel) lê qualquer e-mail do time. Uma chave sending_access lê só os e-mails que ela mesma enviou, pela API ou pelo SMTP, e assim acompanha o status da entrega sem webhooks. Qualquer outro e-mail do time (de outra chave, do painel, de campanhas ou de automações) responde 404 not_found, igual a um e-mail que não existe. As demais rotas de /emails continuam exigindo acesso total.

Parâmetros de caminho

  • idstringobrigatório
    ID do e-mail.

Erros

  • 404not_found
    O e-mail não existe, é de outro time ou, numa chave sending_access, não foi enviado por ela.
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@sa-east-1.amazonses.com>",
  "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,
  "simulated": false,
  "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.
  • 403plan_limit_reached
    O plano não inclui os links para compartilhar e-mails.
  • 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

  • 422validation_error
    limit fora do intervalo ou after e before juntos.
  • 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.

#O objeto e-mail

A lista (GET /emails) e a consulta de um e-mail (GET /emails/{id}) devolvem o mesmo objeto. A consulta acrescenta o histórico e a origem. O envio (POST /emails) devolve só o id, e o envio em lote, uma lista de id. A Postal guarda os e-mails, os anexos e o histórico por 30 dias (e os agendados até saírem); depois disso, a consulta responde 404. Veja Limites.

CampoTipoDescrição
object, idstringemail e o ID do e-mail.
message_idstring | nullO Message-ID do e-mail. É null até o provedor de envio aceitá-lo (e-mails na fila, agendados ou cancelados).
from, to, cc, bcc, reply_tostring | string[]Os endereços, como foram enviados. cc, bcc e reply_to são listas vazias quando não existem.
subject, html, textstring | nullO conteúdo enviado. Num envio com template, é o conteúdo já preenchido com as variáveis.
created_atstringQuando o e-mail foi criado (ISO 8601, UTC).
last_eventstringO status atual: queued, scheduled, sent, delivery_delayed, delivered, opened, clicked, bounced, failed, suppressed, canceled ou complained.
scheduled_atstring | nullA data de envio marcada com scheduled_at, ou null num envio imediato.
simulatedbooleantrue nos e-mails para os endereços de teste, que nunca são enviados de verdade.
tagsobject[]As etiquetas, como [{"name", "value"}].
eventsobject[]Só em GET /emails/{id}: o histórico, do mais antigo para o mais novo, como [{"type", "occurred_at", "data"}]. Veja Eventos de um e-mail.
domain_id, api_key_id, template_idstring | nullSó em GET /emails/{id}: o domínio do remetente, a chave de API usada no envio e o template usado (null quando não houve chave ou template).
source, log_idstringSó em GET /emails/{id}: api ou smtp, e o ID do log da requisição (null se não houver).

#Eventos de um e-mail

O campo events de GET /emails/{id} guarda tudo o que aconteceu com o e-mail, em ordem. Cada evento tem type, occurred_at e data (ou null). Quase todos também podem ser assinados como eventos de webhook (email.canceled e email.unsubscribed existem só no histórico). Os eventos que envolvem vários destinatários listam, em data.recipients, os endereços a que se aplicam.

TipoQuandodata
email.scheduledO e-mail foi criado com scheduled_at.null
email.canceledO agendamento foi cancelado.null
email.sentO provedor de envio aceitou o e-mail.null
email.deliveredO servidor do destinatário recebeu o e-mail.recipients
email.delivery_delayedA entrega foi adiada; novas tentativas acontecem sozinhas.delay_type, recipients
email.bouncedO e-mail voltou. Bounces permanentes entram na lista de supressão.bounce (type, subType, message, diagnosticCode), recipients
email.complainedO destinatário marcou como spam. O endereço entra na lista de supressão.recipients, feedback_type
email.failedNão foi possível enviar (domínio sem verificação, envio pausado, anexo inacessível…).reason
email.suppressedTodos ou parte dos destinatários estavam na lista de supressão e foram descartados no envio. Os demais seguem normalmente.recipients
email.openedO destinatário abriu o e-mail (rastreamento de aberturas ligado no domínio).open (ipAddress, userAgent, timestamp)
email.clickedUm link foi clicado (rastreamento de cliques ligado).click (ipAddress, link, userAgent, timestamp)
email.repliedChegou uma resposta ao e-mail de uma campanha ou automação que detecta respostas.reply (from, subject, message_id, preview, attachments), auto
email.unsubscribedO contato se descadastrou pelo link de um e-mail de campanha.null

O last_event só avança, mas a lista de events guarda todos os eventos, mesmo os que chegam fora de ordem. email.replied e email.unsubscribed não mudam o last_event.

#Endereços de teste

Para testar a integração e os webhooks de ponta a ponta, sem enviar e-mail de verdade e sem arriscar a reputação do seu domínio, envie para os endereços de teste da Postal, no domínio sandbox.postal.so. É a mesma ideia do [email protected] da Resend.

O e-mail é aceito como qualquer outro: a mesma validação, o from de um domínio verificado do time, anexos, templates, tags, scheduled_at, Idempotency-Key, o envio em lote e o SMTP. Ele aparece em GET /emails e no painel, com o histórico de eventos, mas nunca é enviado: no lugar do provedor, a Postal registra os eventos poucos segundos depois, na ordem de um envio real, e os webhooks saem normalmente.

EndereçoEventos, em ordemStatus final
[email protected]email.sent, email.delivereddelivered
[email protected]email.sent, email.bounced (bounce permanente: type Permanent, subType General, message smtp; 550 5.1.1 user unknown)bounced
[email protected]email.sent, email.delivered, email.complainedcomplained
[email protected]email.suppressed (sem email.sent nem email.delivered)suppressed
[email protected]email.sent, email.delivery_delayeddelivery_delayed

Regras

  • Maiúsculas e minúsculas não importam, e dá para acrescentar um rótulo depois de +, com até 64 caracteres entre letras, números, ., _ e -: [email protected]. Use o rótulo para reconhecer cada teste nos eventos.
  • Não misture. Os destinatários de um e-mail (to, cc e bcc) são todos endereços de teste ou todos reais; misturar devolve 422 validation_error. Um endereço do domínio de teste com outro nome, como [email protected], também devolve 422, listando os nomes válidos.
  • Vários endereços de teste no mesmo e-mail são permitidos: cada resultado vira o seu evento, com a lista recipients dos endereços daquele resultado. O status final segue o evento mais avançado (complained vale mais que bounced e suppressed, que valem mais que delivered).
  • Não contam em nada que afete o seu envio real: não usam a cota de envio (diária, mensal ou de envio além da cota), não entram na lista de supressão (um bounced@ ou complained@ não bloqueia nada), não entram nas taxas de bounce e de reclamação que podem pausar os envios, não aparecem nas métricas nem no resumo semanal, e não disparam o aviso de primeiro e-mail.
  • O resto continua valendo: o limite de requisições por segundo, as chaves de API restritas a um domínio e a pausa de envios do time (403 sending_paused).
  • Recurso do plano. Os endereços de teste dependem do recurso test_addresses do plano (veja Limites do plano); sem ele, o envio responde 403 plan_limit_reached.
  • O e-mail traz "simulated": true em GET /emails/{id}. Não é preciso configurar DNS: nada é entregue a esse domínio.
Corpo de POST /emails
{
  "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>",
  "tags": [
    {
      "name": "category",
      "value": "shipping"
    }
  ]
}

Alguns segundos depois, o seu webhook recebe o mesmo evento de um bounce real:

Webhook email.bounced recebido
{
  "type": "email.bounced",
  "created_at": "2026-10-24T13:02:15.004Z",
  "data": {
    "email_id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b",
    "created_at": "2026-10-24T13:02:11.000Z",
    "from": "Loja Exemplo <[email protected]>",
    "to": ["[email protected]"],
    "subject": "Seu pedido #10482 saiu para entrega",
    "tags": {
      "category": "shipping"
    },
    "bounce": {
      "diagnosticCode": ["smtp; 550 5.1.1 user unknown"],
      "message": "smtp; 550 5.1.1 user unknown",
      "subType": "General",
      "type": "Permanent"
    }
  }
}

#Limites do plano

Alguns recursos de envio dependem do plano. Quando o plano não inclui um deles, a API responde 403 plan_limit_reached e a mensagem diz qual recurso falta. O que cada plano inclui está em Limites e em GET /plans.

Recurso do planoChaveOnde vale
Envio em lotebatch_sendingPOST /emails/batch
Agendamento de enviosscheduled_sendingO campo scheduled_at de POST /emails.
Endereços de teste simuladostest_addressessandbox.postal.so
Tamanho dos anexosattachments_mbO total dos attachments: 0 não permite anexos; um número limita o total em MB; sem número vale o limite de 40 MB da plataforma.
Links para compartilhar e-mailsshared_email_linksPOST /emails/{id}/share
Envio por SMTPsmtp_accessSMTP

As cotas de envio são outro limite: quando acabam, a resposta é 429 daily_quota_exceeded ou 429 monthly_quota_exceeded (veja Cotas). O limite diário é o menor entre o do plano e o teto da reputação da conta, que começa em 500 e-mails por dia nas contas novas. Antes de recusar por monthly_quota_exceeded, a Postal usa a reserva do Adicional de envios, se o time tem uma; o limite diário não é levantado por ela. Os e-mails para endereços de teste não usam a cota.

Envios pausados

Quando as taxas de bounce, de reclamação ou de conteúdo rejeitado do time passam do limite (veja Pausa automática), a Postal pausa os envios para proteger a reputação dos seus domínios. Enquanto isso, POST /emails, POST /emails/batch e os envios para endereços de teste respondem 403 sending_paused. Os webhooks sending.paused e sending.resumed avisam quando isso começa e termina (veja Webhooks).

Itens pausados pelo plano

Se o plano muda e o time passa a ter mais domínios, templates, chaves de API, inboxes, webhooks ou outros itens do que o novo plano permite, a Postal não apaga nada: os mais antigos, até o limite, continuam ativos, e os mais novos ficam pausados. Um item pausado continua na lista e pode ser lido, e o campo paused_by_plan dele é true. Alterá-lo ou usá-lo responde 403 paused_by_plan. Para os envios, isso vale para um from de domínio pausado, para um template pausado e para a própria chave de API, quando ela é a pausada: toda requisição feita com ela é recusada. Os itens voltam a ficar ativos assim que o plano permite de novo, ou quando você apaga outros itens.

403Recurso fora do plano
{
  "statusCode": 403,
  "name": "plan_limit_reached",
  "message": "Your plan does not include “Scheduled sending”. Upgrade your plan to use it."
}
403Item pausado pelo plano
{
  "statusCode": 403,
  "name": "paused_by_plan",
  "message": "This item is paused because it is above the limit of your current plan. Upgrade your plan or delete other items to use it again."
}

As mensagens seguem o Accept-Language (sem ele, vêm em inglês); o name não muda.

#Com os SDKs

Os SDKs oficiais têm métodos para enviar (emails.send e emails.batch), consultar e listar (emails.get, emails.list e emails.iterate), reagendar e cancelar (emails.update e emails.cancel) e ler a mensagem original (emails.raw). Os campos têm os nomes da API.

Agendar, consultar e cancelar

Node.js

import { Postal } from 'postal-so'

const postal = new Postal() // lê POSTAL_API_KEY

const { id } = await postal.emails.send({
  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>',
  scheduled_at: new Date(Date.now() + 3600_000),
})

const email = await postal.emails.get(id) // email.last_event e email.events
await postal.emails.update(id, { scheduled_at: '2026-10-26T09:00:00-03:00' })
await postal.emails.cancel(id)

PHP

use Postal\Client;

$postal = new Client(); // lê POSTAL_API_KEY

$result = $postal->emails->send([
    '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>',
    'scheduled_at' => new DateTimeImmutable('+1 hour'),
]);

$email = $postal->emails->get($result['id']); // last_event e events
$postal->emails->update($result['id'], '2026-10-26T09:00:00-03:00');
$postal->emails->cancel($result['id']);