Guias

SDKs: Node.js, PHP e Laravel

Bibliotecas oficiais para enviar e-mails e eventos, manter contatos e conferir webhooks, com erros tipados, idempotência e retentativas.

#Visão geral

As bibliotecas oficiais cuidam do que você escreveria à mão: autenticação, JSON, erros tipados, chave de idempotência, retentativa com espera crescente e a conferência da assinatura dos webhooks. Os campos têm os mesmos nomes da API, então a referência vale direto para o código.

LinguagemPacoteRequisitosInstalação
Node.js e TypeScriptpostal-soNode 18+, ESM, com tipos, sem dependênciasnpm install postal-so
PHP e Laravelpostal-so/postal-phpPHP 8.2+, ext-curl, sem dependências obrigatóriascomposer require postal-so/postal-php

O código está em postal-node e postal-php. O pacote Node inclui também a CLI postal e uma skill para agentes de IA. Quer usar outra linguagem? A API é HTTP com JSON: veja os exemplos em cURL, Python e PHP de cada endpoint.

#Chave de API

Crie uma chave em Chaves de API (ela começa com ps_) e guarde-a em POSTAL_API_KEY. Os dois SDKs leem essa variável quando você não passa a chave. Para sistemas que só disparam e-mails e eventos, use uma chave só de envio. Veja Autenticação.

.env
POSTAL_API_KEY=ps_sua_chave

Só no servidor

Nunca coloque a chave no navegador, em apps ou no repositório: qualquer pessoa poderia copiá-la.

#Enviar um e-mail

O domínio do remetente precisa estar verificado. A resposta traz o id do e-mail; acompanhe o status com emails.get ou com webhooks.

Enviar um e-mail

Node.js

import { Postal } from 'postal-so';

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

const { id } = await postal.emails.send(
  {
    from: 'Minha empresa <[email protected]>',
    to: '[email protected]',
    subject: 'Bem-vindo!',
    html: '<p>Que bom ter você por aqui.</p>',
    text: 'Que bom ter você por aqui.',
  },
  { idempotencyKey: 'welcome-42' },
);

PHP

use Postal\Client;

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

$result = $postal->emails->send([
    'from' => 'Minha empresa <[email protected]>',
    'to' => '[email protected]',
    'subject' => 'Bem-vindo!',
    'html' => '<p>Que bom ter você por aqui.</p>',
    'text' => 'Que bom ter você por aqui.',
], 'welcome-42'); // chave de idempotência

echo $result['id'];

Com um template publicado, mande template: { id, variables } no lugar de html e text. Também funcionam emails.batch (até 100 e-mails numa requisição), emails.update e emails.cancel (reagendar e cancelar), emails.get e emails.list. Veja E-mails.

#Contatos e eventos

O external_id é o ID da pessoa no seu sistema. contacts.upsert cria ou atualiza o contato procurando por id, depois external_id, depois email; repetir a chamada é seguro. Eventos iniciam as automações com aquele gatilho e funcionam com chave só de envio.

Contatos e eventos

Node.js

// Cria ou atualiza o contato pelo ID dele no seu sistema
await postal.contacts.upsert({
  external_id: 'user_42',
  email: '[email protected]', // obrigatório para criar
  first_name: 'Fulano',
  properties: { plan: 'free' },
});

// Conta ao Postal o que aconteceu: inicia as automações do gatilho "signup"
await postal.events.send(
  {
    name: 'signup',
    contact: 'external_id:user_42',
    properties: { plan: 'free', source: 'site' },
  },
  { idempotencyKey: 'signup-42' },
);

PHP

// Cria ou atualiza o contato pelo ID dele no seu sistema
$postal->contacts->upsert([
    'external_id' => 'user_42',
    'email' => '[email protected]', // obrigatório para criar
    'first_name' => 'Fulano',
    'properties' => ['plan' => 'free'],
]);

// Conta ao Postal o que aconteceu: inicia as automações do gatilho "signup"
$postal->events->send([
    'name' => 'signup',
    'contact' => 'external_id:user_42',
    'properties' => ['plan' => 'free', 'source' => 'site'],
], 'signup-42');

Mais métodos: contacts.get, getByExternalId, update, delete, list e iterate (percorre todas as páginas), segments, topics e domains (list, get, verify). Veja Contatos, Eventos e Domínios.

#Verificar webhooks

verifyWebhook (Node.js) e Webhook::verify (PHP) conferem a assinatura Standard Webhooks da entrega, recusam mensagens com mais de 5 minutos de diferença e devolvem o evento. Passe o corpo cru, exatamente como chegou, e o signing_secret (whsec_…). Se a assinatura não conferir, lançam uma exceção: responda 400. Os detalhes do esquema estão em Webhooks.

Verificar uma entrega

Node.js

import express from 'express';
import { verifyWebhook, PostalWebhookVerificationError } from 'postal-so';

const app = express();

// O corpo precisa chegar cru (Buffer) para a assinatura bater.
app.post('/webhooks/postal', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const event = verifyWebhook(req.body, req.headers, process.env.POSTAL_WEBHOOK_SECRET);

    if (event.type === 'email.bounced') {
      // descarte o endereço
    }

    res.sendStatus(200);
  } catch (error) {
    if (error instanceof PostalWebhookVerificationError) {
      return res.sendStatus(400);
    }

    throw error;
  }
});

PHP

use Postal\Exceptions\WebhookVerificationException;
use Postal\Webhook;

try {
    $event = Webhook::verify(
        file_get_contents('php://input'), // corpo cru
        getallheaders(),
        getenv('POSTAL_WEBHOOK_SECRET'),
    );
} catch (WebhookVerificationException $e) {
    http_response_code(400);
    exit;
}

if ($event['type'] === 'email.bounced') {
    // descarte o endereço
}

http_response_code(200);

#Erros e retentativas

Toda resposta de erro vira uma exceção com o statusCode, o name (o código estável da lista de erros, como validation_error) e a message do Postal. Trate pelo name, não pela mensagem. Falhas de rede e tempo esgotado viram erros com statusCode 0 e name network_error ou timeout.

Tratar erros

Node.js

import { PostalError } from 'postal-so';

try {
  await postal.emails.send(params);
} catch (error) {
  if (error instanceof PostalError) {
    console.error(error.statusCode, error.name, error.message);
    // 422 validation_error Missing `subject` field.

    if (error.name === 'daily_quota_exceeded') {
      // a cota diária acabou; volta às 00:00 UTC
    }
  }

  throw error;
}

PHP

use Postal\Exceptions\PostalException;

try {
    $postal->emails->send($params);
} catch (PostalException $e) {
    $e->getStatusCode(); // 422
    $e->getErrorName();  // 'validation_error'
    $e->getMessage();    // 'Missing `subject` field.'

    if ($e->getErrorName() === 'daily_quota_exceeded') {
        // a cota diária acabou; volta às 00:00 UTC
    }
}

Retentativas e idempotência

  • Por padrão, os SDKs repetem até 2 vezes, com espera de 500 ms que dobra a cada tentativa (no máximo 30 s). Mude com maxRetries (Node.js) ou max_retries (PHP); 0 desliga.
  • 429 rate_limit_exceeded é repetido sempre, esperando o Retry-After. Cota esgotada (daily_quota_exceeded, monthly_quota_exceeded) não é repetida.
  • 5xx e falhas de rede são repetidos em leituras, em exclusões, em contacts.upsert e em envios com chave de idempotência. Um POST sem chave, como contacts.create, não é repetido, para não duplicar o efeito.
  • Em emails.send, emails.batch e events.send, o SDK gera uma chave de idempotência para a chamada e a reaproveita nas retentativas. Para proteger também a sua repetição (um job que roda de novo), passe uma chave que identifique o motivo do envio, como order-10482-shipped.

#Laravel

O pacote PHP se registra sozinho no Laravel (auto-discovery). Ele cria o Postal\Client no container, lendo POSTAL_API_KEY, e um transporte de e-mail chamado postal: com ele, Mail::to(…)->send(…), notificações e Mailables saem pela API do Postal, sem mudar o código da aplicação.

Defina o mailer no .env e declare o transporte em config/mail.php:

.env
MAIL_MAILER=postal
[email protected]
MAIL_FROM_NAME="Minha empresa"
POSTAL_API_KEY=ps_sua_chave
config/mail.php
// config/mail.php
'mailers' => [
    'postal' => [
        'transport' => 'postal',
        // 'key' => env('POSTAL_API_KEY'), // opcional: uma chave só deste mailer
    ],
],

O domínio de MAIL_FROM_ADDRESS precisa estar verificado. Vão junto os anexos, as imagens embutidas, Cc, Bcc, Reply-To, os cabeçalhos extras e as etiquetas (tag() e metadata() do Mailable viram etiquetas do envio). O ID do e-mail no Postal fica em $sentMessage->getMessageId().

Para eventos e contatos, injete o cliente:

app/Listeners/SendSignupEvent.php
use Postal\Client;

class SendSignupEvent
{
    public function __construct(private Client $postal) {}

    public function handle(Registered $event): void
    {
        $user = $event->user;

        $this->postal->contacts->upsert([
            'external_id' => (string) $user->id,
            'email' => $user->email,
            'first_name' => $user->name,
        ]);

        $this->postal->events->send(
            ['name' => 'signup', 'contact' => 'external_id:'.$user->id],
            'signup-'.$user->id,
        );
    }
}

Quem prefere SMTP puro pode usar o SMTP do Postal no Laravel; o transporte postal usa a API e devolve o ID do e-mail.

#Agentes de IA

O pacote Node.js traz uma skill (skills/postal/SKILL.md) que ensina Claude Code, Codex, Cursor e outros agentes a integrar o Postal no seu app: instalar o SDK, guardar a chave, enviar e-mails, mandar eventos de cadastro e pagamento, sincronizar contatos e verificar webhooks. Copie a pasta skills/postal para a pasta de skills da sua ferramenta (por exemplo, .claude/skills/) ou peça ao agente para ler o arquivo.

Há também um /llms.txt, com o resumo da documentação e os links das páginas, para agentes que leem a documentação direto.