# PennyPost API documentation

Base URL: `https://api.pennypost.io` · Sign up: https://app.pennypost.io · SDK: `npm install pennypost`

## Quickstart

```js
import { PennyPost } from "pennypost";
const pp = new PennyPost("pp_test_<secret>"); // instant at signup
const { accepted } = await pp.emails.send({
  from: "Receipts <receipts@yourdomain.com>",
  to: ["customer@example.com"],
  subject: "Your order shipped",
  text: "Tracking inside.",
});
const email = await pp.emails.get(accepted[0].id);
```

## Authentication

Bearer token. `pp_test_<secret>` = test mode (no email delivered, no domain needed, no caps, rows marked `"mode":"test"`). `pp_live_<secret>` = real sending from verified domains, issued automatically when your first domain verifies. Both read logs and suppressions. Keys are shown once and stored hashed.

Test-mode magic recipients: `fail@<any>` fails permanently; `throttle@<any>` fails retryable.

## Send

`POST /v1/emails` — fields: `from`, `to` (ALWAYS an array, 1–50), `subject`, `html` and/or `text`, optional `reply_to`, `tags` (≤10), `metadata`, `headers`. Optional `Idempotency-Key` header makes retries safe.

Response 201: `{ accepted: [{to,id}], suppressed: [{to,reason}], quarantined?: [{to,reason}], failed?: [{to,id}] }`. Suppressed/quarantined recipients are reported, never sent, never charged.

## Retrieve and list

`GET /v1/emails/:id` — email + `events` timeline. Statuses: accepted → sent → delivered, or bounced / complained / failed (terminal statuses win).
`GET /v1/emails?to=&limit=&cursor=` — most-recent first, ≤100/page, 30-day retention on every plan.

## Suppressions

Automatic on hard bounce and complaint. `GET /v1/suppressions` · `POST /v1/suppressions {email}` · `DELETE /v1/suppressions/:email`.

## Webhooks

POST /v1/webhooks `{url, events?}` registers an https endpoint (max 5; public hostnames only), returns `{id, url, events, secret: "whsec_…", status}`. Events: `email.delivered|email.bounced|email.complained|email.failed` (default all). GET /v1/webhooks lists. DELETE /v1/webhooks/:id removes. POST /v1/webhooks/:id/test fires a signed `webhook.test`.

Delivery: POST with header `pennypost-signature: t=<unix>,v1=<hex hmac-sha256(secret, t + "." + rawBody)>`; verify with 5-min tolerance. At-least-once, dedupe on event `id`. 2xx within 5s = received; retries at +1m and +15m; 20 consecutive failures disables the endpoint (delete + recreate to re-enable). Payload: `{id: "evt_…", type, created_at, data: {email_id, to, from, subject, mode, code, reason}}`.

## Domains and going live

Add your registrable domain in the dashboard (subdomains roll up; free tier is per registrable domain; 1 domain card-free, 5 with a card; each domain belongs to one account). You get 6 DNS records (3 signing CNAMEs, return-path MX+TXT on `send.<domain>`, DMARC starter). On first verification your `pp_live_` key is issued automatically, shown once.

## Errors

Shape: `{ error: { type, code, message, param?, retryable } }`. Codes include `validation_failed` (422), `domain_not_verified` (422), `daily_cap_reached` (429, retryable), `account_paused` (401, the notice email has the reason), `send_failed` (502).

## Sending policy

New senders: 500/day, auto-ramping ~5×/week on clean metrics to 50,000 (500 → 2,500 → 12,500 → 50,000). Enforcement is automated and always explained: warn (with numbers) → restrict (quarantines NEW recipients only; existing recipients keep receiving) → pause (with reason). Appeals: dev@pockadot.com. Cold outreach and purchased lists are banned.
