HTTP API
The Postwind API is REST JSON under /api/v1. The contract is an OpenAPI 3.1 document generated from the code and served by every node at GET /api/openapi.json; the reference below is rendered from a copy of that document. Authenticate with Authorization: Bearer pw_…. Errors are problem+json (RFC 9457) with a stable code.
Before you start
You need a running Postwind node with the api subservice. The API listener answers on port 8080 (api.listen = "0.0.0.0:8080"). It is an internal listener: it speaks plain HTTP and expects a TLS terminator in front of it. Built-in TLS is optional, through api.tls with a certificate file and a key file. The OpenAPI document has no servers entry, so the base URL is whatever your operator gives the node.
To run a node, see Download and the deployment guide. The examples on these pages use two shell variables:
export POSTWIND_URL=https://mail.example.com # your node, behind its TLS terminator
export POSTWIND_KEY=pw_adm_… # the key string, see below
First request
On a node with the node’s config, postwind keys create-admin prints a new admin key. The key string goes to stdout once, with nothing else, so KEY=$(postwind keys create-admin) works. The flag --name sets the key’s name (1 to 128 characters, default admin); --expires-at takes an RFC 3339 time after which the key stops working. In Docker Compose:
docker compose exec postwind postwind keys create-admin
For automation there is a second way: the operator makes an admin key string pw_adm_<prefix>_<secret> and points security.bootstrap_admin_key_file at it. The node registers it at start when the storage has no admin key at all.
Ask the API which key you hold:
curl -H "Authorization: Bearer $POSTWIND_KEY" "$POSTWIND_URL/api/v1/whoami"
{
"key_id": "01900000-0000-7000-8000-000000000001",
"name": "ops",
"kind": "admin",
"project_id": null,
"scopes": ["read", "write", "send", "keys"],
"expires_at": null
}
An admin key has no project and holds all four scopes. Then GET /api/v1/server returns the node’s version, enabled subservices, transports, DNS providers and IP pools. Both operations take any valid key: whoami and get_server_info.
Conventions
- The base path is
/api/v1. Request and response bodies are JSON,Content-Type: application/json. - Ids are UUID strings. Times are RFC 3339 in UTC.
- A field without a value is
null. It is never left out: every answer of one operation has the same set of keys. - Enumerations are
snake_casestrings. - Creating answers
201with the resource and aLocationheader. Changing isPATCHwith a partial body. Deleting answers204. - An unknown path under
/api/v1is404 not_found, before the key is checked. - Incompatible changes go only under a new prefix,
/api/v2.
Authentication and keys
Admin keys do everything, including projects and admin keys. Project keys work in one project within their scopes: read, write, send, keys. A key string is shown once, at creation; the server stores only a hash of its secret.
Every request carries Authorization: Bearer pw_…. A missing, invalid, revoked or expired key gets 401 with one of four codes and a WWW-Authenticate: Bearer header. A key without the scope an operation needs gets 403 forbidden, and the detail names the scope. Each operation page shows what the operation needs. See Authentication and scopes.
Project context
Project resources live under /api/v1/projects/{project_id}/…. A project key gets 404 for another project’s resources, the same body as for a missing one, so nothing about other projects is revealed. An admin key works with any project.
The check for another project comes before the check of scopes. So the answer about another project is the same whatever scopes the key holds.
Errors
Every error is application/problem+json: type (https://postwind.dev/errors/<code>), title, status, detail, code, and on 422 an errors array of { "field", "code", "message" }. The code is stable and the same in MCP tool errors.
The errors page has every code, every field code and the order in which a request is checked.
Idempotency
Idempotency-Key on every POST: 1–128 visible ASCII characters, a UUID will do. A repeat with the same key and body within 24 h returns the first answer with Idempotent-Replayed: true; the same key with another body is 422 idempotency_key_reused. Only 2xx answers are kept.
The key belongs to the API key that sent it. Without the header a POST runs every time it arrives. See Idempotency.
Pagination
Lists take ?limit= (default 50, max 500) and ?cursor= and answer { "items": [...], "next_cursor": "..." | null }. The cursor is opaque and the same in the API and in MCP.
next_cursor is null exactly on the last page. Rows added between two pages are not repeated and not skipped. See Pagination.
Rate limits
api.rate_limit_per_minute requests a minute per key per node (default 600), one budget for the API and MCP. Over it: 429 with Retry-After in seconds.
A refused request does not spend the budget. See Rate limits.
Storage timeouts
A storage call that does not answer within storage.clickhouse.query_timeout_secs (default 5 s) gives 504 storage_timeout. The outcome is unknown, not undone: check before you repeat a POST without an Idempotency-Key.
GET, PATCH and DELETE can be repeated: a repeat gives the same result. A repeat of a POST with the same Idempotency-Key never runs it twice; it gets 409 idempotency_key_in_progress until the key’s 24 h end. Check whether the resource exists and go on with a new key.
Limits
Request body up to 10 MB. Subscriber import up to 10 000 records per request.
A larger body is 413 body_too_large; the same number applies to MCP. See Request bodies for the content type, the strict form of a body and PATCH.
The OpenAPI document
Your node serves the document at GET /api/openapi.json without a key. The reference on this site is rendered from a copy of the same document; the commit and the date of that copy are at the bottom of each page. The raw copy is at /docs/api/openapi.json, for client generators and validators.
Next steps
- Create a project with an admin key:
POST /api/v1/projects(create_project). - Create a project key with the scopes it needs:
POST /api/v1/projects/{project_id}/keys(create_api_key). - Add a sending domain and verify it (domains).
- Create a list and add subscribers (lists, subscribers).
- Create a template (templates), then a campaign; preflight it and launch it (campaigns).
Every operation is also an MCP tool with the same name. See MCP.
Guides
Reference
| Name | Operation | Description |
|---|---|---|
| Server | 2 operations | The node and the calling key |
| Projects | 5 operations | Projects, the units of isolation |
| Keys | 6 operations | API keys: a project’s keys, and admin keys |
| Lists | 5 operations | Mailing lists of a project |
| Subscribers | 11 operations | Subscribers of a project’s lists |
| Suppressions | 3 operations | The project’s suppression list: addresses that get no mail |
| Domains | 8 operations | Sending domains, their DKIM keys and DNS records |
| Templates | 7 operations | Message templates and their versions |
| Campaigns | 10 operations | Campaigns: a template sent to lists of the project |