For developers
Send email from your code, the way you already do.
REST API with the same formats as Resend, SMTP, SDKs for Node.js, PHP and Laravel, and a CLI. To migrate, you can usually just swap the base URL and the key.
Create a key
Use one key per system and per environment. The checkout, WordPress or the ERP only need to send: give them a send-only key, tied to the right domain.
- If a send-only key leaks, it can only send and see what it sent itself
- The key is shown only once, at creation
- Send-only keys tied to a domain depend on the plan
Send the first email
All paths go through the same rules: verified domain, quotas, suppression list and the same events. Choose where to send from.
The API you already know
One POST to /emails and the email goes out. Field names and response and error formats are Resend's, so what you already know still applies.
- Batch sending, up to 100 emails per request
- Scheduling up to 30 days ahead, and cancellation (batch and scheduling depend on the plan)
- Idempotency-Key to repeat a call without duplicating the send
curl -X POST https://api.postal.so/emails \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Aurora Cosméticos <[email protected]>",
"to": ["[email protected]"],
"subject": "Your order #10482 is out for delivery",
"html": "<p>Order <b>#10482</b> is on its way.</p>"
}'SDK for Node.js and TypeScript
The postal-so package has types, typed errors, retry with exponential backoff and webhook signature verification. It works on Node 18 or newer, with no dependencies.
- npm install postal-so
- Reads the key from POSTAL_API_KEY
- Includes the postal CLI for testing from the terminal
import { Postal } from 'postal-so';
const postal = new Postal(); // reads POSTAL_API_KEY
const { id } = await postal.emails.send(
{
from: 'Aurora Cosméticos <[email protected]>',
to: '[email protected]',
subject: 'Your order #10482 is out for delivery',
html: '<p>Order <b>#10482</b> is on its way.</p>',
},
{ idempotencyKey: 'order-10482-shipped' },
);SDK for PHP and transport for Laravel
The postal-so/postal-php package requires PHP 8.2 or newer. In Laravel, it creates the client on its own and ships the postal transport: just declare the mailer in config/mail.php and use MAIL_MAILER=postal, and Mail::to() stays the same.
- composer require postal-so/postal-php
- Mailables, notifications, attachments and tags keep working
- Prefer SMTP? Laravel also works with Postal's SMTP server
use Postal\Client;
$postal = new Client(); // reads POSTAL_API_KEY
$result = $postal->emails->send([
'from' => 'Aurora Cosméticos <[email protected]>',
'to' => '[email protected]',
'subject' => 'Your order #10482 is out for delivery',
'html' => '<p>Order <b>#10482</b> is on its way.</p>',
], 'order-10482-shipped'); // idempotency key
echo $result['id'];SMTP for whatever doesn't speak API
WordPress, ERPs, store platforms and printers send through Postal's SMTP server, with an API key as the password. The same API rules apply: verified domain, quotas and suppression list. SMTP depends on the plan.
- Port 465 (recommended) or 587; 2465 and 2587 if your network blocks them
- The response code carries the email id
- Up to 40 MB and 50 recipients per message
Server smtp.postal.so
Port 465 (direct TLS) or 587 (STARTTLS)
User postal
Password an API key (ps_...)
# Laravel, in .env
MAIL_MAILER=smtp
MAIL_SCHEME=smtps
MAIL_HOST=smtp.postal.so
MAIL_PORT=465
MAIL_USERNAME=postal
MAIL_PASSWORD=ps_your_keyFollow along in the logs
Every request made with an API key and every message sent over SMTP appears under Logs, with the body sent and the response. Logs are kept for 30 days.
Receive the events
Receive every email event on your server, with a Standard Webhooks signature, up to 8 attempts and manual resend. The SDK verifies the signature in one line.
- 30 event types, from emails to contacts and inboxes
- Same webhook-id on every attempt, so you can discard duplicates
- Test event and test addresses, such as [email protected]
import express from 'express';
import {
verifyWebhook,
PostalWebhookVerificationError,
} from 'postal-so';
const app = express();
// The body must arrive raw (Buffer)
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') {
// remove the address
}
res.sendStatus(200);
} catch (error) {
if (error instanceof
PostalWebhookVerificationError) {
return res.sendStatus(400);
}
throw error;
}
},
);Test without really sending
Test addresses generate real events and webhooks, don't use the quota and don't affect your reputation. They depend on the plan. The CLI helps you test from the terminal, with JSON output for scripts.
| Test address | Events it generates |
|---|---|
[email protected] | email.sent and email.delivered |
[email protected] | email.sent and email.bounced, with a permanent bounce |
[email protected] | email.sent, email.delivered and email.complained |
[email protected] | Only email.suppressed |
[email protected] | email.sent and email.delivery_delayed |
npm install --global postal-so postal login postal emails send \ --from [email protected] \ --to [email protected] \ --subject "Teste" --text "Funcionou"
Migrate from Resend
Postal's API uses the same features, field names and response formats, so migrating usually means swapping the base URL and the key. The Resend SDK keeps working if you point the base URL to Postal.
- Tested with the resend 6.34 package for Node.js
- What changes: the secret and signing rules for webhooks, limits and quotas
- curl -X POST https://api.resend.com/emails \- -H "Authorization: Bearer re_..."- - # ou, com o SDK da Resend, no .env- RESEND_BASE_URL=https://api.resend.com+ curl -X POST https://api.postal.so/emails \+ -H "Authorization: Bearer ps_..."+ + # ou, com o SDK da Resend, no .env+ RESEND_BASE_URL=https://api.postal.so| Item | On Postal |
|---|---|
| Base URL and key | https://api.postal.so and Authorization: Bearer ps_… |
| Sending and errors | POST /emails returns {"id"} and errors follow {statusCode, name, message}. Same. |
| Event names | The same ones (email.delivered, email.bounced…), plus others, such as email.replied and contact.*. |
| Webhooks | Standard Webhooks format: webhook-* and svix-* headers. The secret (whsec_…) belongs to each endpoint. |
| Test addresses | [email protected] becomes [email protected], and so on. |
| Limits and quotas | Per team and per plan. See Limits. |
Before you flip the key
- Domain verified on Postal (you can keep the same one on both platforms at the same time)
- API key created: send-only and tied to the domain, for anything that only sends
- Suppression list imported, 100 addresses per request
- Webhook created and signature verified before you switch traffic
- Base URL and key swapped first in the test environment
Codes the app fills in
The OTP-Token header carries the code and the originating site. It is still an IETF draft and not every email client reads it; those that don't know it ignore it.
From: Aurora Cosméticos <[email protected]> Subject: Your access code OTP-Token: "123456"; origin="https://app.exemplo.com.br"
Documentation
Everything in the documentation
Guides, the complete API reference and examples in cURL, Node.js, Python and PHP.
Documentation
Getting started, guides and concepts.
Open the documentationAPI reference
Every route, with examples and responses.
See the referenceSDKs and CLI
Node.js, PHP, Laravel and the command line.
See the SDKsSMTP
Server, ports, user and response codes.
See SMTPMigrate from Resend
What stays the same, what changes and how to switch traffic over.
Read the guideOTP-Token header
Format, sending, reading and security.
Read the guideFrequently asked questions
Can I keep using the Resend SDK?
Yes. Point the base URL to https://api.postal.so and use the Postal key. We tested the resend 6.34 package for Node.js; other SDKs that let you change the URL speak the same format, but have not been tested one by one.
When should I use SMTP and when the API?
Use SMTP in systems that don't talk to APIs, such as WordPress and ERPs. Use the API when you need tags, scheduling or templates, which SMTP doesn't have.
What is the request limit?
It applies per team and per second, and depends on the plan. If you go over the limit, the response is 429, with the Retry-After header. The SDKs wait and retry on their own. See Limits.
How do I test without sending a real email?
Send to the test addresses, such as [email protected], if your plan includes them. The email is simulated, doesn't use the quota and generates real events and webhooks.