API documentation
Send and receive email, manage domains and follow every delivery with a simple REST API, with the option to process your email in São Paulo.
#Overview
The Postal API is a REST API with JSON. You send transactional emails, receive messages in your inboxes, manage domains, templates, webhooks and the suppression list, take care of the marketing audience (contacts, segments, topics and sign-up forms), send campaigns, build automations from your product events and query logs and metrics, all with one API key. If you prefer SMTP, use the same key at smtp.postal.so.
- Getting startedVerify a domain, create a key and send your first email in five minutes.
- AuthenticationFull-access or sending-only keys, optionally restricted to one domain.
- Dashboard guideWhat each dashboard screen and button does: emails, audience, campaigns, domains, settings and more.
- WebhooksGet deliveries, bounces, opens and received emails in real time.
- SMTPFor systems that only speak SMTP:
smtp.postal.sowith your key. - OTP-Token headerOne-time codes the mail client can autofill: format, sending, reading and security.
- Domains and DNSThe DKIM, SPF and MX records, sending region and tracking.
- Integrate your appStripe, Supabase, Clerk, Auth0 and Firebase become contacts and events; sign-in emails go out through Postal.
- ErrorsThe format of error responses and what to do with each one.
- SDKsNode.js, PHP and Laravel:
npm install postal-soorcomposer require postal-so/postal-php. - CLIThe
postalcommand to send emails and events from the terminal. - Migrate from ResendSame features and formats: swap the URL and the key and see what changes.
#Base URL
Every request goes to:
https://api.postal.so
Always use HTTPS. Request and response bodies are JSON (Content-Type: application/json), in UTF-8. The exceptions are image uploads, in multipart/form-data, and the original messages (/raw), in plain text.
#Your first request
With a key in POSTAL_API_KEY and a verified domain, sending is just this:
cURL
curl -X POST https://api.postal.so/emails \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme Store <[email protected]>",
"to": ["[email protected]"],
"subject": "Your order #10482 is out for delivery",
"html": "<p>Order <b>#10482</b> is on its way.</p>"
}'
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',
},
body: JSON.stringify({
from: 'Acme Store <[email protected]>',
to: ['[email protected]'],
subject: 'Your order #10482 is out for delivery',
html: '<p>Order <b>#10482</b> is on its way.</p>',
}),
});
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']}"},
json={
"from": "Acme Store <[email protected]>",
"to": ["[email protected]"],
"subject": "Your order #10482 is out for delivery",
"html": "<p>Order <b>#10482</b> is on its way.</p>",
},
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')],
'json' => [
'from' => 'Acme Store <[email protected]>',
'to' => ['[email protected]'],
'subject' => 'Your order #10482 is out for delivery',
'html' => '<p>Order <b>#10482</b> is on its way.</p>',
],
]);
$data = json_decode($response->getBody(), true);
{
"id": "019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b"
}
The full walkthrough, with the domain and key setup, is in Getting started.
#Conventions
- IDs are time-ordered UUIDs, like
019a1b2c-4d5e-7f60-8a9b-0c1d2e3f4a5b. - Dates come in ISO 8601, in UTC (
2026-10-24T13:02:11.000000Z). In the ones you send, include the time zone, like2026-10-25T09:00:00-03:00. - Objects carry an
objectfield (email,domain,list…) that says what the response is. - Lists use cursor pagination, with
has_more. - Deletions answer
{"object": "…", "id": "…", "deleted": true}. - Errors always follow the
{"statusCode", "name", "message"}format. See Errors. - Field and error names are in English, to make searching and integrating easier. The error
messagecomes in English, or in Portuguese withAccept-Language: pt-BR.
#API resources
The reference is grouped by resource. Each endpoint has its parameters, examples in cURL, Node.js, Python and PHP, the response and the possible errors.
#Data retention
Sent emails (with content, attachments and events), webhook deliveries and API logs are kept for 30 days. After that they are deleted automatically. If you need a longer history, keep the events you receive through webhooks or use the exports.