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.
| Linguagem | Pacote | Requisitos | Instalação |
|---|---|---|---|
| Node.js e TypeScript | postal-so | Node 18+, ESM, com tipos, sem dependências | npm install postal-so |
| PHP e Laravel | postal-so/postal-php | PHP 8.2+, ext-curl, sem dependências obrigatórias | composer 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.
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.
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.
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.
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.
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) oumax_retries(PHP);0desliga. 429 rate_limit_exceededé repetido sempre, esperando oRetry-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.upserte em envios com chave de idempotência. UmPOSTsem chave, comocontacts.create, não é repetido, para não duplicar o efeito. - Em
emails.send,emails.batcheevents.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, comoorder-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:
MAIL_MAILER=postal
[email protected]
MAIL_FROM_NAME="Minha empresa"
POSTAL_API_KEY=ps_sua_chave
// 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:
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.