Authentication
Authenticate every request with an API key in the Authorization header. Keys carry a permission, a live or test mode, and optionally a single domain they may send from.
API keys#
Send your key as a Bearer token on every request to https://api.poststack.dev. Keys look like sk_live_ or sk_test_ followed by 64 hex characters.
import { PostStack } from '@poststack.dev/sdk';
const poststack = new PostStack(process.env.POSTSTACK_API_KEY!);curl https://api.poststack.dev/emails \
-H "Authorization: Bearer $POSTSTACK_API_KEY"import os
from poststack import PostStack
ps = PostStack(api_key=os.environ["POSTSTACK_API_KEY"])client := poststack.NewClient(os.Getenv("POSTSTACK_API_KEY"))A missing or wrong key on a GET returns 401 with a WWW-Authenticate: Bearer header. On POST, PATCH, PUT and DELETE, a request with no Authorization: Bearer header is treated as a browser request and refused with 403 Invalid request origin before the key is even looked at. The scheme is case-sensitive: write Bearer.
Create a key#
Open API keys
In the dashboard, go to Configuration → API keys and choose Create API key. Your email address must be verified first.
Choose what the key may do
Give it a Name, a Permission (Full access or Sending only), a Mode (Live or Test) and, optionally, a domain under Restrict to domain.
Copy the key
The full key is shown once. PostStack stores only a hash, so a lost key cannot be recovered — rotate it to get a new one.
How many keys a team can hold depends on the plan: 5 on Free, 10 on Starter, 50 on Pro, 1,000 on Scale and unlimited on Enterprise.
Permissions#
| Permission | Dashboard label | Can do |
|---|---|---|
full_access | Full access | Everything the API offers: emails, domains, contacts, templates, broadcasts, webhooks, mailboxes, workflows, and managing API keys. |
sending_access | Sending only | Send, list, read, cancel and reschedule emails; preview emails; read, add and remove suppressions; validate addresses; reply to and forward inbound email; the hosted MCP server’s send and read tools; and list the team’s keys (names and prefixes only). |
A sending-only key calling anything else gets 403 { "error": "insufficient_scope", "required": [...], "missing": [...] }. Use sending-only keys in application servers that just send mail; keep full-access keys for setup scripts and infrastructure tools.
Domain-restricted keys#
A key restricted to one domain can only send from addresses on that domain. Any other from is refused with 403 API key is restricted to a different domain. Without a restriction a key may send from every domain on the team.
One restricted key per customer or per app is the safest setup: a leaked key can only ever send as its own domain, which keeps many domains under one account isolated from each other. Deleting a domain expires the keys restricted to it immediately, so a restricted key never silently becomes an unrestricted one.
Test keys#
A key created in Test mode starts with sk_test_. It runs every check a live key does but never delivers anything, and its sends do not count against your plan. See Test mode.
OAuth access tokens#
Third-party integrations do not handle your API keys: you authorise them through PostStack's OAuth flow and they receive an access token starting with pat_, sent the same way (Authorization: Bearer pat_…). A token can only do what its scopes allow — for example email:send, domains:read or contacts:manage — and an endpoint outside them returns 403 insufficient_scope. An expired or revoked token returns 401 Invalid or expired access token.
Managing keys over the API#
These endpoints need a full-access key, a dashboard session, or an OAuth token with api-keys:manage. Reading the key list is also open to sending-only keys.
Create an API key#
/api-keysMint a new key. The response is the only time the secret is shown.
{
"name": "Production app server",
"permission": "sending_access",
"mode": "live",
"domain_id": "dom_8k2m4q7x1c5w9r3t6z0p2n4v"
}HTTP/1.1 201 Created
{
"key": "sk_live_3f9a2c7e1b8d4f6a0c9e5b2d7f1a3c8e6b4d9f2a7c1e5b8d3f6a0c9e2b7d4f1a",
"id": 214,
"name": "Production app server",
"permission": "sending_access",
"mode": "live",
"keyPrefix": "sk_live_3f9a"
}const { key, id } = await poststack.apiKeys.create({
name: 'Production app server',
permission: 'sending_access',
domain_id: 'dom_8k2m4q7x1c5w9r3t6z0p2n4v',
});curl -X POST https://api.poststack.dev/api-keys \
-H "Authorization: Bearer $POSTSTACK_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Production app server", "permission": "sending_access" }'Body parameters
namestringrequired- 1–100 characters.
permission"full_access" | "sending_access"required- What the key may do.
mode"live" | "test"default"live"- Test keys simulate sends and never deliver.
domain_idstring- Restrict the key to one of your domains, by
dom_…id or numeric id. Omit (or send an empty string) for an unrestricted key. allow_full_accessbooleandefaultfalse- Must be
trueto create afull_accesskey while authenticated with an API key — a guard against scripts minting powerful keys by accident.
Response fields
keystring- The secret key. Shown only in this response — store it now; it cannot be retrieved later.
idinteger- The key’s id, used by get, rotate and revoke.
namestring- The name you gave it.
permission"full_access" | "sending_access"- What the key may do.
mode"live" | "test"- Live keys send; test keys simulate.
keyPrefixstring- The first 12 characters (sk_live_3f9a), shown in the dashboard to tell keys apart.
Status codes
| Status | Meaning |
|---|---|
| 201 | Created. Store key now. |
| 400 | Validation failed (missing name, unknown permission or mode). |
| 401 | Invalid, revoked or expired credentials. |
| 403 | insufficient_scope — a sending-only key, or an OAuth token without api-keys:manage. Also Invalid request origin when the request has no Authorization: Bearer header. |
| 403 | Also: your email address is not verified; the plan's key limit is reached (API key limit reached for free plan (5 keys)); a full_access key requested with an API key but without allow_full_access; or a full_access key requested by an OAuth client, which is never allowed. |
| 404 | Domain not found — domain_id is not one of your domains. |
| 429 | More than 10 key operations per minute. |
| 500 | Unexpected server error. |
List API keys#
/api-keysAll of the team's keys, oldest first. Secrets are never returned.
{
"keys": [
{
"id": 214,
"name": "Production app server",
"keyPrefix": "sk_live_3f9a",
"permission": "sending_access",
"mode": "live",
"domainId": 41,
"domainName": "acme.com",
"lastUsedAt": "2026-09-29T09:58:12.004Z",
"expiresAt": null,
"createdAt": "2026-08-14T12:00:00.000Z"
}
]
}const { keys } = await poststack.apiKeys.list();curl https://api.poststack.dev/api-keys \
-H "Authorization: Bearer $POSTSTACK_API_KEY"Response fields
keys[].idinteger- Key id.
keys[].namestring- Key name.
keys[].keyPrefixstring- First 12 characters of the key.
keys[].permissionstring- full_access or sending_access.
keys[].modestring- live or test.
keys[].domainIdinteger | null- Restricted domain, or null.
keys[].domainNamestring | null- That domain’s name.
keys[].lastUsedAtstring | null- Last successful authentication.
keys[].expiresAtstring | null- Set when the key was expired (its restricted domain was deleted).
keys[].createdAtstring- Creation time.
Status codes
| Status | Meaning |
|---|---|
| 200 | The keys. The list is not paginated. |
| 401 | Invalid, revoked or expired credentials. |
| 403 | An OAuth token without api-keys:manage. |
| 500 | Unexpected server error. |
Retrieve an API key#
/api-keys/:idOne key's metadata. The secret is never returned.
{
"id": 214,
"name": "Production app server",
"keyPrefix": "sk_live_3f9a",
"permission": "sending_access",
"mode": "live",
"domainId": 41,
"lastUsedAt": "2026-09-29T09:58:12.004Z",
"expiresAt": null,
"createdAt": "2026-08-14T12:00:00.000Z"
}const key = await poststack.apiKeys.get(214);curl https://api.poststack.dev/api-keys/214 \
-H "Authorization: Bearer $POSTSTACK_API_KEY"Path parameters
idintegerrequired- The key id.
Response fields
(object)object- The same fields as a list entry, without
domainName, and not wrapped in an envelope.
Status codes
| Status | Meaning |
|---|---|
| 200 | The key. |
| 400 | id is not a positive integer. |
| 401 | Invalid, revoked or expired credentials. |
| 403 | An OAuth token without api-keys:manage. |
| 404 | API key not found. |
| 500 | Unexpected server error. |
Rotate an API key#
/api-keys/:id/rotateReplace a key with a new secret that keeps its name, permission, mode and domain. The old key stops working immediately and the new one has a new id.
HTTP/1.1 201 Created
{
"key": "sk_live_9c1e4b7d2f8a5c3e0b6d9f1a4c7e2b8d5f0a3c6e9b1d4f7a2c5e8b0d3f6a9c1e",
"id": 231,
"name": "Production app server",
"permission": "sending_access",
"mode": "live",
"keyPrefix": "sk_live_9c1e"
}const { key } = await poststack.apiKeys.rotate(214);curl -X POST https://api.poststack.dev/api-keys/214/rotate \
-H "Authorization: Bearer $POSTSTACK_ADMIN_KEY"Path parameters
idintegerrequired- The key to replace.
Response fields
keystring- The secret key. Shown only in this response — store it now; it cannot be retrieved later.
idinteger- The key’s id, used by get, rotate and revoke.
namestring- The name you gave it.
permission"full_access" | "sending_access"- What the key may do.
mode"live" | "test"- Live keys send; test keys simulate.
keyPrefixstring- The first 12 characters (sk_live_3f9a), shown in the dashboard to tell keys apart.
Status codes
| Status | Meaning |
|---|---|
| 201 | Rotated. Deploy the new key; the old one is already dead. |
| 401 | Invalid, revoked or expired credentials. |
| 403 | insufficient_scope — a sending-only key, or an OAuth token without api-keys:manage. Also Invalid request origin when the request has no Authorization: Bearer header. |
| 403 | Also: an OAuth client rotating a full_access key, which is never allowed. |
| 404 | API key not found. |
| 429 | More than 10 key operations per minute. |
| 500 | Unexpected server error. |
Revoke an API key#
/api-keys/:idDelete a key. Requests made with it fail with 401 from the moment this returns.
{
"success": true
}await poststack.apiKeys.revoke(214);curl -X DELETE https://api.poststack.dev/api-keys/214 \
-H "Authorization: Bearer $POSTSTACK_ADMIN_KEY"Path parameters
idintegerrequired- The key to revoke.
Response fields
successtrue- The key is gone.
Status codes
| Status | Meaning |
|---|---|
| 200 | Revoked. |
| 401 | Invalid, revoked or expired credentials. |
| 403 | insufficient_scope — a sending-only key, or an OAuth token without api-keys:manage. Also Invalid request origin when the request has no Authorization: Bearer header. |
| 404 | API key not found. |
| 429 | More than 10 key operations per minute. |
| 500 | Unexpected server error. |
SMTP credentials#
The SMTP relay authenticates with the same keys:
| Setting | Value |
|---|---|
| Host | smtp.poststack.dev |
| Port | 587 (STARTTLS or implicit TLS) |
| Username | poststack — not checked when the password is an API key |
| Password | Your API key (sk_live_… or sk_test_…) |
Troubleshooting#
401 Invalid API key
Check for a truncated key or a stray space or quote from your secret store. A key that used to work was revoked, rotated, or restricted to a domain that has since been deleted. The dashboard's API keys page shows each key's prefix and when it was last used.
403 Invalid request origin on a POST
The Authorization header did not arrive, or does not start with exactly Bearer . Also call https://api.poststack.dev, not http://: plain HTTP answers with a redirect, and many clients follow a redirected POST as a GET without its body.
403 insufficient_scope
The key is Sending only and the endpoint manages resources, or an OAuth token lacks the scope named in missing. Use a full-access key for that call.