PineMail

PineMail API

Transactional send from your app. A project, a verified from-domain, a Bearer key, POST /v1/emails. 202 means queued. JSON is snake_case. Errors are RFC 7807 application/problem+json. Public URL is https://pinemail.app/v1/… (also served at /api/v1). Contract: /openapi.yaml.

Authentication

Create a key under the project’s API keys page. Pass it as a Bearer token. Keys start with pm_live_. The full token is shown once.

http
Authorization: Bearer pm_live_...

Access levels

Each key is either full access or a restricted set of scopes. A restricted key that hits an endpoint it does not own receives 403. Existing keys without a stored scope list keep full access.

ScopeWhat it allows
emails:sendPOST /v1/emails
emails:readGET /v1/emails, GET /v1/emails/{id}
domains:readList and fetch sending domains
domains:writeAdd, verify, or delete a domain
mailboxes:readList and fetch mailboxes
mailboxes:writeCreate, update, or delete a mailbox
webhooks:readList webhooks and delivery history
webhooks:writeCreate, edit, test, or delete a webhook
templates:readList and fetch templates
directory:readList the organisation address book
directory:writeAdd or remove a directory person

Send an email

from must use a verified domain on the project — your domain after DNS, or @pinemail.app immediately. Display names are kept (Acme <hello@pinemail.app>). Provide text or html. A 202 means queued — poll GET /v1/emails/{id} or watch the project email log. Retry a POST only with the same Idempotency-Key. Attachments are not on this endpoint; use the mailbox compose flow for that.

bash
curl -X POST https://pinemail.app/v1/emails \ -H "Authorization: Bearer pm_live_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: welcome-42" \ -d '{ "from": "Acme <hello@pinemail.app>", "to": ["ada@example.com"], "subject": "Welcome", "html": "<p>Hello from your app.</p>", "reply_to": "support@example.com", "tags": ["onboarding"] }'

Emails

GET /v1/emails lists newest first. Optional limit (1–200) and cursor. GET /v1/emails/{id} includes the event timeline.

Domains

Add a sending domain, publish the returned DNS records, then verify. The hosted pinemail.app domain cannot be removed.

bash
curl -X POST $API/v1/domains \ -H "Authorization: Bearer pm_live_..." \ -H "Content-Type: application/json" \ -d '{"domain":"mail.example.com"}' curl -X POST $API/v1/domains/$ID/verify \ -H "Authorization: Bearer pm_live_..."

Mailboxes

Inbox addresses on a domain you already added. Each one gets its own login: the address and password sign in to the web client and to mail apps, and see only that mailbox. If you omit password, PineMail generates one and returns it once. spam_level is off, low, medium, high, or null to inherit the project default.

bash
curl -X POST $API/v1/mailboxes \ -H "Authorization: Bearer pm_live_..." \ -H "Content-Type: application/json" \ -d '{ "domain_id": "dom_...", "local_part": "hello", "display_name": "Hello", "quota_mb": 1024, "daily_send_limit": 100 }'

Templates

GET /v1/templates and GET /v1/templates/{id} return saved subjects and bodies plus discovered {{variables}}.

Directory

Organisation address book, including people shared in from other projects. Members are synced from mailboxes and cannot be deleted through the API.

bash
curl $API/v1/directory \ -H "Authorization: Bearer pm_live_..." curl -X POST $API/v1/directory \ -H "Authorization: Bearer pm_live_..." \ -H "Content-Type: application/json" \ -d '{"name":"Ada Lovelace","email":"ada@example.com","title":"Engineer"}'

Webhooks

Subscribe an HTTPS URL to email lifecycle events. Signing, payload shape, retries, and the test endpoint are documented on /docs/webhooks.

bash
curl -X POST $API/v1/webhooks \ -H "Authorization: Bearer pm_live_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/pinemail", "events": ["email.sent", "email.delivered", "email.bounced", "email.failed"] }'

Errors

Every error uses application/problem+json with type, title, status, and usually detail. Validation failures are 422 and include an errors map. Rate limits are 429 with Retry-After.

json
{ "type": "/errors/forbidden", "title": "Forbidden", "status": 403, "detail": "This API key is missing the emails:send scope." }

Status codes

200 / 201 / 202 / 204 success. 400 bad request. 401 missing or invalid key. 403 missing scope. 404 unknown resource. 422 validation. 429 rate limited. 500 unexpected.