1. Introduction
This service gives you programmable email inboxes: create mailboxes via the API, receive real emails from any sender over SMTP, then read them back through the REST API or get pushed notifications via webhooks.
- Create and manage mailboxes via API
- Receive emails from any sender over SMTP (port 25)
- Webhooks for real-time notifications
- Attachment download support
- API-key authentication
2. Authentication
By default this instance runs in public UI mode (PUBLIC_UI=true): all endpoints
work without an API key, and a supplied key is still validated. Set PUBLIC_UI=false for
private mode, where every endpoint except GET /status requires one of these methods:
# Method 1: custom header
X-API-Key: pkmail_your_api_key
# Method 2: bearer token
Authorization: Bearer pkmail_your_api_key
3. Base URL
https://your-domain.example/api/v1
In the examples below, $BASE means your base URL and $KEY your API key.
4. Create mailbox
POST/mailboxes
| Field | Type | Required | Description |
|---|---|---|---|
local | string | Yes | Local part: a-z 0-9 . _ -, max 64 chars |
domain | string | No | Must be a served domain. Defaults to the first configured domain |
label | string | No | Human-readable label, max 200 chars |
metadata | object | No | Custom JSON metadata |
curl -X POST $BASE/mailboxes \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"local":"otp001","label":"Signup OTP"}'
# → 201 Created, or 200 with "is_new": false if it already exists
5. List mailboxes
GET/mailboxes?page=1&limit=20&search=otp&source=api&active=true
| Param | Default | Description |
|---|---|---|
page | 1 | Page number |
limit | 20 | Items per page (max 100) |
search | — | Search address or label |
source | — | Filter by source: api or smtp |
active | — | Filter by active status (true/false) |
Each item includes email_count and last_email_at. The response envelope
is { "success": true, "data": [...], "pagination": { "page": 1, "limit": 20, "total": 5, "total_pages": 1 } }.
6. Get mailbox detail
GET/mailboxes/:address
The address must be URL-encoded, e.g. otp001%40mail.example.com.
curl "$BASE/mailboxes/otp001%40mail.example.com" -H "X-API-Key: $KEY"
7. Update mailbox
PATCH/mailboxes/:address
curl -X PATCH "$BASE/mailboxes/otp001%40mail.example.com" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"label":"New label","metadata":{"note":"updated"},"is_active":true}'
| Field | Type | Description |
|---|---|---|
label | string | null | Update or clear the label |
metadata | object | null | Update or clear metadata |
is_active | boolean | Reactivate (true) or disable (false) |
8. Delete mailbox
DELETE/mailboxes/:address?delete_emails=true&hard=true
| Param | Default | Description |
|---|---|---|
delete_emails | false | Also delete all emails and attachment files |
hard | false | Permanently delete the record (default: soft-disable) |
curl -X DELETE "$BASE/mailboxes/otp001%40mail.example.com?hard=true&delete_emails=true" \
-H "X-API-Key: $KEY"
9. List emails in a mailbox
GET/mailboxes/:address/emails?page=1&limit=20&since=2026-01-01T00:00:00Z&search=code
| Param | Description |
|---|---|
page / limit | Pagination (default 1 / 20, max 100) |
since | ISO date — only emails received after this |
search | Search subject or sender |
10. Search all emails
GET/emails?search=code&from=noreply&to=otp&since=2026-01-01&until=2026-12-31
Searches across all mailboxes.
| Param | Description |
|---|---|
search | Search subject, sender, or recipient |
from | Filter by sender address (partial match) |
to | Filter by recipient address (partial match) |
since / until | ISO dates bounding received_at |
11. Get email detail
GET/emails/:id
Returns the full message: text_body, html_body (sanitized, safe to render),
html_body_raw (original), and an attachments array with download_url entries.
curl "$BASE/emails/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -H "X-API-Key: $KEY"
12. Delete email
DELETE/emails/:id
Deletes the email and removes its attachment files from disk.
13. Purge emails
DELETE/mailboxes/:address/emails?older_than_minutes=60
Deletes emails in a mailbox without disabling it. Attachment files are cleaned up too.
| Param | Required | Description |
|---|---|---|
older_than_minutes | No | Only delete emails older than N minutes. Omit to delete ALL |
14. Download attachment
GET/attachments/:id/download
Returns the binary file with Content-Disposition: attachment. Requires
ATTACHMENTS_ENABLED=true on the server, otherwise 403.
curl -OJ "$BASE/attachments/att_xxxxxxxxxxxxxxxxxxxxxxxx/download" -H "X-API-Key: $KEY"
15. Webhooks
Register URLs to receive real-time event notifications. Deliveries are POSTed as JSON
with an X-Webhook-Signature: sha256=<hmac> header (HMAC-SHA256 of the raw JSON body
using your webhook secret), retried 3 times with exponential backoff.
Events
| Event | Trigger |
|---|---|
email.received | A new email arrives in any mailbox |
mailbox.created | A mailbox is created via the API |
mailbox.deleted | A mailbox is disabled or permanently deleted |
* | Subscribe to all events |
Endpoints
POST /webhooks # register
GET /webhooks # list
GET /webhooks/:id # detail + recent deliveries
PATCH /webhooks/:id # update
DELETE /webhooks/:id # delete
POST /webhooks/:id/test # send a test delivery
Register
curl -X POST $BASE/webhooks \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"url":"https://your-app.example/hooks/mail","events":["email.received"],"secret":"s3cr3t","label":"Prod"}'
Payload
{
"event": "email.received",
"timestamp": "2026-01-01T12:00:00.000Z",
"data": {
"email_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"mailbox_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
"to_address": "otp001@mail.example.com",
"from_address": "noreply@example.org",
"from_name": "Example",
"subject": "Your verification code is 123456",
"received_at": "2026-01-01T12:00:00.000Z",
"has_attachments": false
}
}
Verifying the signature (Node.js)
const crypto = require('crypto');
const signature = req.headers['x-webhook-signature'];
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== expected) throw new Error('Invalid signature');
16. Service status
GET/status public — no auth required
curl $BASE/status
Returns service state, version, uptime, the served domain, SMTP host/port, feature flags,
and stats (active_mailboxes, total_emails, emails_received_today).
17. Errors
All errors share one shape:
{ "success": false, "error": { "code": "UNAUTHORIZED", "message": "Invalid API key." } }
| Code | HTTP | Description |
|---|---|---|
BAD_REQUEST | 400 | Invalid request |
UNAUTHORIZED | 401 | Missing or invalid API key |
FORBIDDEN | 403 | Access denied (e.g. attachments disabled) |
NOT_FOUND | 404 | Resource not found |
VALIDATION_ERROR | 422 | Validation failed |
RATE_LIMITED | 429 | Too many requests |
INTERNAL_ERROR | 500 | Server error |
18. Rate limits
Enabled with RATE_LIMIT_ENABLED=true. Limits are per API key (per IP for public endpoints):
| Endpoint | Limit | Window |
|---|---|---|
| Create mailbox | 60 requests | 1 minute |
| List emails | 120 requests | 1 minute |
| Status (public) | 60 requests | 1 minute |
19. Integration examples
cURL — full flow
# 1. Create a mailbox
curl -X POST $BASE/mailboxes \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"local":"otp001"}'
# 2. Send a real email to otp001@mail.example.com, then read it:
curl "$BASE/mailboxes/otp001%40mail.example.com/emails" -H "X-API-Key: $KEY"
# 3. Get one email in full
curl "$BASE/emails/<email-id>" -H "X-API-Key: $KEY"
# 4. Register a webhook
curl -X POST $BASE/webhooks \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"url":"https://your-app.example/hooks/mail","events":["email.received"]}'
Node.js
const BASE = process.env.MAIL_API_BASE; // https://your-domain.example/api/v1
const KEY = process.env.MAIL_API_KEY;
const headers = { 'X-API-Key': KEY, 'Content-Type': 'application/json' };
// Create a mailbox
const mb = await fetch(`${BASE}/mailboxes`, {
method: 'POST', headers,
body: JSON.stringify({ local: 'otp001', label: 'Signup OTP' }),
}).then((r) => r.json());
// Poll for the newest email
const emails = await fetch(
`${BASE}/mailboxes/${encodeURIComponent(mb.data.address)}/emails?limit=1`,
{ headers }
).then((r) => r.json());
console.log(emails.data[0]);