API reference

The postie handbook

postie is a JSON REST API. Every request goes to the base URL below with your API key. No SDK needed: anything that can make an HTTP request can send email. Base URL: https://api.postie.sh

No. 01

Quickstart

1. Sign up, add your domain and set its DNS records. 2. Create an API key. 3. Send your first email:

const res = await fetch("https://api.postie.sh/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.EMAIL_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "Shop <orders@yourshop.com>",
    to: "customer@example.com",
    subject: "Order #1042 confirmed",
    html: "<p>Thanks for your order!</p>",
    tags: [{ name: "kind", value: "order_confirmation" }],
  }),
});

const { id } = await res.json();

Or from a terminal:

curl -X POST https://api.postie.sh/emails \
  -H "Authorization: Bearer po_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"from":"orders@yourshop.com","to":"customer@example.com","subject":"Hi","text":"Hello"}'

A successful send returns { "id": "…" }. Use that ID with GET /emails/:id to check its status.

No. 02

Authentication

Every request needs an Authorization: Bearer po_… header. Keys are shown once when you create them, and only a SHA-256 hash is stored.

PermissionCan do
full_accessEvery endpoint: send, list, domains and API keys
sending_accessPOST /emails and POST /emails/batch only. Can be limited to one domain with domain_id

No. 03

Endpoints

MethodPathDescription
POST/emailsSend an email
POST/emails/batchSend up to 100 emails. Set x-batch-validation to strict (default) or permissive
GET/emailsList emails (limit, after)
GET/emails/:idGet an email and its last event
PATCH/emails/:idReschedule a scheduled email
POST/emails/:id/cancelCancel a scheduled email
POST/domainsAdd a domain
GET/domainsList domains
GET/domains/:idGet a domain and its DNS records
DELETE/domains/:idRemove a domain
POST/domains/:id/verifyRe-check DNS verification
POST/api-keysCreate an API key (token returned once)
GET/api-keysList API keys
DELETE/api-keys/:idDelete an API key

POST /emails accepts from, to, subject, html and/or text, cc, bcc, reply_to, headers, attachments (base64 content or a path URL), tags and scheduled_at. Batch sends don’t support attachments or scheduling.

No. 04

Idempotency

Send an Idempotency-Key header, e.g. your order ID. Retrying with the same key returns the original email ID instead of sending again. In a batch, each email gets key:index.

await fetch("https://api.postie.sh/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.EMAIL_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `order-${order.id}`,
  },
  body: JSON.stringify(email),
});

No. 05

Scheduling

Set scheduled_at to an ISO 8601 date or plain English such as in 1 hour, in 30 minutes or in 2 days. It can be up to 30 days ahead. Reschedule with PATCH /emails/:id or cancel with POST /emails/:id/cancel.

No. 06

Domains and DNS

When you add a domain, postie sets it up for sending and returns the records to add at your DNS provider. Verified domains also cover their subdomains.

RecordTypeNameValue
DKIM ×3CNAME<token>._domainkey<token>.dkim.amazonses.com
SPFMXbounce10 feedback-smtp.eu-west-2.amazonses.com
SPFTXTbouncev=spf1 include:amazonses.com ~all
DMARC (optional)TXT_dmarcv=DMARC1; p=none;

The DKIM records sign your mail. The bounce. MX and TXT records set a custom MAIL FROM domain, so bounces come back to us and SPF aligns. Pending domains are checked every 15 minutes, or right away with POST /domains/:id/verify.

No. 07

Webhooks

Add endpoints in the dashboard and choose which events to receive. Each delivery is a JSON { type, created_at, data } body.

email.sentemail.deliveredemail.delivery_delayedemail.bouncedemail.complainedemail.openedemail.clickedemail.failedemail.suppressed

Every delivery carries three headers: webhook-id, webhook-timestamp (unix seconds) and webhook-signature. The signature is v1, followed by a base64 HMAC-SHA256 of ${id}.${timestamp}.${body}, keyed with your signing secret base64-decoded after the whsec_ prefix. Reject requests older than five minutes.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(req: Request, body: string, secret: string) {
  const id = req.headers.get("webhook-id") ?? "";
  const ts = req.headers.get("webhook-timestamp") ?? "";
  const sig = req.headers.get("webhook-signature") ?? "";
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const want = createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest("base64");
  return sig.split(" ").some((s) => {
    const got = s.replace(/^v1,/, "");
    return got.length === want.length && timingSafeEqual(Buffer.from(got), Buffer.from(want));
  });
}

Failed deliveries (anything other than a 2xx) retry six times over about 17 hours. Every delivery, its response and a replay button are in the dashboard.

No. 08

Errors

Errors always return the same shape, with a stable name you can switch on:

{ "statusCode": 422, "message": "Missing `to` field.", "name": "validation_error" }
StatusNameWhen
401missing_api_keyNo Authorization header
403invalid_api_keyThe key doesn’t exist or was deleted
401restricted_api_keyA sending key called a management endpoint
403restricted_api_keyA domain-scoped key sent from another domain
422validation_errorInvalid body, address or scheduled_at
403validation_errorThe from domain isn’t verified
404not_foundThe email, domain or key doesn’t belong to your team
403team_pausedSending was paused because the bounce or complaint rate got too high
429monthly_quota_exceededThis send would go over your monthly quota
409concurrent_idempotent_requestsSame idempotency key is still being processed
502application_errorThe sending service couldn’t complete the request
500internal_server_errorSomething unexpected went wrong

No. 09

Limits

LimitValue
Recipients per email (to + cc + bcc)50
Emails per batch100
Attachments per email20, 10MB total
Tags per email10
Scheduling window30 days
Free plan3,000 emails / month, 100 / day

Monthly volume, domains and webhook endpoints depend on your plan. See pricing for every plan's limits.

Addresses that hard bounce or complain are suppressed automatically. Sending to them returns an ID with status suppressed, and nothing is sent.