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.

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

FieldTypeRequiredDescription
localstringYesLocal part: a-z 0-9 . _ -, max 64 chars
domainstringNoMust be a served domain. Defaults to the first configured domain
labelstringNoHuman-readable label, max 200 chars
metadataobjectNoCustom 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

ParamDefaultDescription
page1Page number
limit20Items 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}'
FieldTypeDescription
labelstring | nullUpdate or clear the label
metadataobject | nullUpdate or clear metadata
is_activebooleanReactivate (true) or disable (false)

8. Delete mailbox

DELETE/mailboxes/:address?delete_emails=true&hard=true

ParamDefaultDescription
delete_emailsfalseAlso delete all emails and attachment files
hardfalsePermanently 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

ParamDescription
page / limitPagination (default 1 / 20, max 100)
sinceISO date — only emails received after this
searchSearch 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.

ParamDescription
searchSearch subject, sender, or recipient
fromFilter by sender address (partial match)
toFilter by recipient address (partial match)
since / untilISO 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.

ParamRequiredDescription
older_than_minutesNoOnly 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

EventTrigger
email.receivedA new email arrives in any mailbox
mailbox.createdA mailbox is created via the API
mailbox.deletedA 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." } }
CodeHTTPDescription
BAD_REQUEST400Invalid request
UNAUTHORIZED401Missing or invalid API key
FORBIDDEN403Access denied (e.g. attachments disabled)
NOT_FOUND404Resource not found
VALIDATION_ERROR422Validation failed
RATE_LIMITED429Too many requests
INTERNAL_ERROR500Server error

18. Rate limits

Enabled with RATE_LIMIT_ENABLED=true. Limits are per API key (per IP for public endpoints):

EndpointLimitWindow
Create mailbox60 requests1 minute
List emails120 requests1 minute
Status (public)60 requests1 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]);