Docs
Owlpost developer docs
Owlpost is one HTTP API for sending email, receiving it, and giving AI agents inboxes of their own. The sending API takes Resend's request shapes, so most Resend code moves by changing a base URL and a key.
These pages describe what the code does today. Every endpoint carries one of two labels:
- Built implemented and running on our staging deployment. It is not publicly reachable yet.
- Planned not built. Each one links to the issue that tracks it.
The basics
| Thing | Value |
|---|---|
| Base URL | https://api.owlpost.to (planned). Every path starts with /v1. |
| Authentication | Authorization: Bearer <key> on every request |
| Bodies | JSON in, JSON out. Errors are application/problem+json (Errors). |
| Timestamps | RFC 3339 in UTC, to the second: 2026-10-03T09:00:00Z |
| Ids | A prefix and a lowercase ULID: em_ sent email, dm_ domain, ib_ inbox, im_ received message, rt_ routing rule |
| Request id | Every response carries x-request-id. Error bodies repeat it as instance. Quote it when you write to us. |
API keys and scopes
A key belongs to one account. There are two kinds:
op_live_…sends real mail, and only from domains you verified.op_test_…runs the whole pipeline (validation, storage, suppression, the message's status) and never hands a message to a mail server. Use it in development and CI. See Test keys.
We store a hash of the key, never the key itself. It is shown once, when it is issued. During early access we issue keys by hand: write to hello@owlpost.to. Sign-up and a dashboard for keys are Planned.
A key carries scopes, and a route answers 403 to a key without its scope. A new key gets all six unless we narrow it.
| Scope | Allows |
|---|---|
emails:send | Send and batch-send, reply from an inbox, add and remove suppressions, name topics |
emails:read | Read a sent message, list suppressions |
domains:manage | Add, verify, list and remove sending domains |
webhooks:manage | Register, list and remove webhook endpoints; dead letters and replay |
inbound:read | List, search and read received mail, threads, raw messages and routing rules |
inbound:manage | Create and delete inboxes and routing rules; review, release and delete held mail |
Keys scoped to a single agent inbox are Planned (#1).
Quickstart
Four steps: a key, a domain, a first email, then an inbox for an agent. Put your key in an environment variable so it stays out of your code:
export OWLPOST_API_KEY=op_test_… # the key we sent you1. Verify a sending domain
Skip this while you use a test key: test mail may name any sender. Live mail needs a verified domain.
curl https://api.owlpost.to/v1/domains \
-H "Authorization: Bearer $OWLPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "example.com"}'The answer lists six DNS records: three DKIM CNAMEs, an MX and a TXT for the bounces subdomain, and a recommended DMARC TXT. Add them at your DNS host, then ask Owlpost to check (it also rechecks pending domains every few minutes on its own):
curl -X POST https://api.owlpost.to/v1/domains/dm_…/verify \
-H "Authorization: Bearer $OWLPOST_API_KEY"When status reads verified, live mail can leave from that domain and its subdomains. Details: Domains.
2. Send your first email
curl https://api.owlpost.to/v1/emails \
-H "Authorization: Bearer $OWLPOST_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: welcome-ada-1" \
-d '{
"from": "Acme <hello@example.com>",
"to": "ada@example.org",
"subject": "Welcome to Acme",
"html": "<p>Hi Ada, thanks for signing up.</p>",
"text": "Hi Ada, thanks for signing up."
}'// Node 18+ (fetch is built in). No SDK needed.
const res = await fetch('https://api.owlpost.to/v1/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.OWLPOST_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'welcome-ada-1',
},
body: JSON.stringify({
from: 'Acme <hello@example.com>',
to: 'ada@example.org',
subject: 'Welcome to Acme',
html: '<p>Hi Ada, thanks for signing up.</p>',
text: 'Hi Ada, thanks for signing up.',
}),
});
if (!res.ok) throw new Error(JSON.stringify(await res.json()));
const { id } = await res.json(); // "em_01j9…"# pip install requests
import os
import requests
res = requests.post(
"https://api.owlpost.to/v1/emails",
headers={
"Authorization": f"Bearer {os.environ['OWLPOST_API_KEY']}",
"Idempotency-Key": "welcome-ada-1",
},
json={
"from": "Acme <hello@example.com>",
"to": "ada@example.org",
"subject": "Welcome to Acme",
"html": "<p>Hi Ada, thanks for signing up.</p>",
"text": "Hi Ada, thanks for signing up.",
},
)
res.raise_for_status()
print(res.json()["id"]) # "em_01j9…"The answer is 200 with {"id": "em_…"}. Owlpost has stored the message; it goes to the mail server a moment later, so a provider hiccup never turns into an API error. Read its progress with GET /v1/emails/em_…, or hear about it by webhook. Owlpost SDKs are Planned; meanwhile the Resend Node SDK works against Owlpost.
3. Give an agent an inbox
curl https://api.owlpost.to/v1/inbound/inboxes \
-H "Authorization: Bearer $OWLPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "support"}'{
"object": "inbox",
"id": "ib_01j9…",
"address": "support-x7k2qp@agents.owlpost.to",
"name": "support",
"ai_footer": true,
"created_at": "2026-10-03T09:00:00Z"
}Mail sent to that address is parsed, screened and stored. Read it by polling, or have it pushed to you:
curl "https://api.owlpost.to/v1/inbound/messages?inbox=ib_01j9…&limit=10" \
-H "Authorization: Bearer $OWLPOST_API_KEY"curl https://api.owlpost.to/v1/webhooks \
-H "Authorization: Bearer $OWLPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"endpoint": "https://agent.example.com/hooks/owlpost", "events": ["message.received"]}'
# Store the signing_secret in the answer: it is shown once.4. Reply as the agent
curl https://api.owlpost.to/v1/inbound/messages/im_01j9…/reply \
-H "Authorization: Bearer $OWLPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Thanks, your booking is confirmed for Friday."}'The reply goes out from the inbox's address, in the same thread, marked as written by an AI. Everything about inboxes is on Inbound.
Coming
Not built yet. Each item links to the issue that tracks it.
- CLI and terminal mail client: run
owlpostto open a full mail client for your agent inboxes (threads, reply and compose in$EDITOR, held mail with its reason, release or delete, attachments with their safety verdict, offline cache, notifications). Every action is also a scriptable subcommand with--json, plus log tailing and webhook forwarding to localhost. #13 - MCP server: every API function as agent tools. #14
GET /v1/emails: list sent mail with cursor pagination. #12- Inbox-scoped keys: an API key that can only touch one agent inbox. #1
- Long poll: wait for the next message in an inbox instead of polling. #3
- Handoff notices: deliver a message into an agent inbox by API, without an SMTP round trip. #2
- Webhook endpoint updates and secret rotation, with a window where both secrets verify. #10
- Webhook test events: signed events for test-key mail and a test send. #11
- Retention and erasure: a retention period for stored messages and an API to erase one recipient's data. #9
- Production:
api.owlpost.toitself, with deploys and a runbook. #8 - Also planned, not yet filed as issues: SDKs for Node.js, Python, Go, Rust, PHP and Ruby; an OpenAPI document; sign-up, a dashboard and billing; templates; attachments fetched from a URL.