# milbox Public API > Domain inventory, temporary addresses, and inbox for products and resellers. One key per account, bound to an IP allowlist and scoped to the addresses it created. Reference for humans and LLM agents: 51 endpoints, base URL `https://milbox.dev/api/v1`. Human docs: https://milbox.dev/developers/api. Machine spec: https://milbox.dev/openapi.json. Version 1.6.0 (stable), panel v2.2.0, build c0de095. Generated 2026-11-03. ## Overview - Base URL: `https://milbox.dev/api/v1` - Auth: send the key in the `x-api-key` header (`Authorization: Bearer ` works too). Keys are hashed server-side; there is no login, cookie, or session on this surface. - IP scope: keys are bound to an IP allowlist at creation. Calls from other IPs fail with 403 ip_not_allowed before touching a route. - Versions: panel `2.2.0`, API `1.6.0` (stable). Compatible API minors: `1.0.0`, `1.1.0`, `1.2.0`, `1.3.0`, `1.4.0`, `1.5.0`, `1.6.0`. Every response carries x-api-version. - Rate limit: per-key requests-per-minute budget or unlimited. Every response carries x-ratelimit-limit / x-ratelimit-remaining ("unlimited" when uncapped); exceeding it returns 429 with retry_after seconds. - Success envelope: `{"ok": true, "data": ..., "meta": {...}}`. - Error envelope: `{"ok": false, "error": {"code", "message", ...}}`. - Message shape: `from` is the sender address string, `from_name` is the display name, `otp` is the one-time code extracted from the subject or body (null when there is none, safe to poll instead of writing your own parser), bodies are `text` and `html`, the Date header is `headers_date` + `headers_date_valid`, and `received_chain`, `dkim_signatures`, `authentication_results` (one joined string), `raw_available` sit at the top level of GET /messages/{id}. Lists carry `read_at` and `starred_at`, never seen/starred. - Idempotency: GET and DELETE are safe to retry. POST /addresses and POST /webhooks honour an `idempotency-key` header: replaying the same key within 24h returns the stored response with `idempotent-replay: true` instead of creating a duplicate. ## Quickstart 1. Create a key in the panel (Developers → API keys) and allowlist your server IP, or mark the key unrestricted. 2. Probe availability: `GET /domains/check?domain=example.com` → reason "available" means safe to register. 3. Register: `POST /domains {"domain":"example.com"}` → response contains the TXT challenge (host, value, 72h expiry). 4. Publish the TXT record, then `POST /domains/{id}/verify` → domain flips to verified/active. 5. Create addresses: `POST /addresses {"domain":"example.com","ttl_minutes":30}` → address + expiry timestamp. 6. Poll the inbox: `GET /inbox?address=...&unread=true` or wait on `since_id`; fetch bodies via `GET /messages/{id}`. 7. Get the badge numbers with `GET /inbox/counts`, then clear them with `POST /inbox/read` (optionally `{"ids":[...]}`) or put them back with `POST /inbox/unread`. 8. Stop polling: `POST /webhooks {"url":...,"events":["message.received"]}` and verify each delivery against the `x-milbox-signature` HMAC. 9. Attachments are two calls, one router each: `GET /inbox/attachments` lists every file across the mailbox with its `checksum_sha256`, then `GET /messages/{id}/attachment/{checksum}` streams that one file. `/messages/{id}/download` returns the whole raw .eml instead. 10. Tear down: `DELETE /addresses/{id}` or `DELETE /domains/{id}` (soft delete; the purge cron hard-deletes later). ```bash curl -X POST https://milbox.dev/api/v1/addresses \ -H "x-api-key: MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"domain":"milbox.dev","local_part":"signup","ttl_minutes":30}' ``` ## Conventions - Envelope success: { "ok": true, "data": { }, "meta": { "request_id": "e7c87866405f3959", "timestamp": "2026-10-20T19:51:35.267Z" } } - Envelope error: { "ok": false, "error": { "code": "ip_not_allowed", "message": "Request rejected: the source address is outside the allowlist configured for this key.", "details": { "ip_allowlist": ["203.0.113.10/32"] }, "links": [ { "rel": "docs", "href": "/developers/api#sec-allowlist" } ] }, "meta": { "request_id": "8c813885fd030cc7", "timestamp": "2026-10-20T19:51:32.519Z", "docs_url": "/developers/api" } } - Pagination: `page` + `per_page` query params; `meta` carries total/pages. Inbox listings also return `meta.next_cursor` for keyset pagination; pass it back as `?cursor=` to page without offset drift while new mail arrives. - Search: `?q=` matches subject, from name, from address, and the decoded text body. - Scopes: keys carry a comma separated scope list (`*` by default). A write scope implies the matching read scope. Missing scope returns 403 insufficient_scope with the required_scopes detail. - Method errors: an unsupported HTTP method returns a JSON 405 with an `allow` header, never an HTML page. - Timestamps are ISO-8601 UTC. Expiry is enforced server-side; expired addresses reject mail and are hard-deleted ~5 minutes after expiry (no trace). - Address TTL: `ttl_minutes` (min 1). Omitted → the domain `mail_cap_hours` applies; when that is null the address lives until the domain dies. Requests exceeding the cap fail 422 ttl_exceeds_domain. - Optional address password: supply `password` at creation, then pass `?password=` on GET /addresses/{id}, GET /messages/{id}, DELETE /messages/{id} and GET /inbox?address=...; wrong or missing password returns 404 (no existence leak). Listing /inbox without an address hides password-protected addresses. - Aliases: pass `alias_of` to attach a new local part to an existing address group; the group shares one inbox. ## Endpoints ### Account What this key is allowed to do. Call it once on boot to fail fast on a bad or moved IP. #### GET /account Describe the key itself: label, premium scope, allowlist, budget, and usage counters. ```bash curl https://milbox.dev/api/v1/account \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "label": "reseller-alpha", "key_prefix": "mml_9c1f4a2b", "premium_access": true, "rate_limit_per_minute": 120, "ip_allowlist": ["203.0.113.0/24", "198.51.100.7/32"], "ip_unrestricted": false, "expires_at": null, "created_at": "2026-10-20T19:43:09.000Z", "last_used_at": "2026-10-20T20:12:55.000Z", "request_count": 4182, "usage": { "total": 4182, "last_24h": 640, "denied_24h": 3 } } } ``` Errors: `401 missing_api_key`, `401 invalid_api_key`, `403 ip_not_allowed`. #### GET /account/quota Address quota, message totals, retention, and the key profile in one call. ```bash curl https://milbox.dev/api/v1/account/quota \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "quota": { "domain_limit": 5, "domain_used": 2, "address_limit": 50, "address_used": 12, "message_limit": 1000, "message_used": 42, "unlimited": false, "premium": false } } } ``` Errors: `401 invalid_api_key`. #### GET /account/usage Everything a client needs to self-throttle: scopes, request counters for 24h and 7d, the twenty busiest endpoints with their error counts, address quota, message counters, and the effective retention window. ```bash curl https://milbox.dev/api/v1/account/usage \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "key": { "label": "reseller-alpha", "key_prefix": "mml_9c1f4a2b", "scopes": ["addresses:read", "inbox:read"], "premium_access": false, "rate_limit_per_minute": 120, "expires_at": null }, "requests": { "total": 4182, "last_24h": 640, "last_7d": 3100, "denied_24h": 3 }, "endpoints": [{ "path": "/api/v1/inbox", "hits": 2200, "errors": 12 }], "addresses": { "limit": 50, "used": 12, "remaining": 38, "unlimited": false }, "messages": { "total": 42, "unread": 7, "read": 35, "deleted": 3, "attachments": 5, "starred": 2, "retention_hours": 48 }, "webhooks": 2 }, "meta": { "request_id": "e7c87866405f3959", "timestamp": "2026-10-20T19:51:35.267Z", "capabilities": { "retention_hours": 48, "max_message_bytes": 10485760 } } } ``` Errors: `401 invalid_api_key`. #### GET /meta Machine-readable capabilities document: api version, operation count per group, the full error-code list, request and response headers, pagination style, and the webhook event catalogue. Useful for client bootstrapping without fetching openapi.json. ```bash curl "https://milbox.dev/api/v1/meta" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "api_version": "1.5.0", "operations": 33, "groups": [{ "id": "group-inbox", "name": "Inbox", "count": 12 }], "error_codes": ["missing_api_key", "insufficient_scope", "rate_limited"], "pagination": { "style": "offset + cursor", "cursor_field": "next_cursor", "cursor_param": "cursor" }, "webhooks": { "events": ["message.received", "address.created", "address.expired", "message.deleted"], "signature_header": "x-milbox-signature", "algorithm": "hmac-sha256(timestamp.body)" } } } ``` #### GET /keys Introspect the presented key: scopes, premium flag, budgets, quotas, effective retention, status, and the lifetime request counter. Read-only by design, keys are provisioned from the panel. ```bash curl "https://milbox.dev/api/v1/keys" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "id": 1, "label": "reseller-alpha", "key_prefix": "mml_9c1f4a2b", "scopes": ["*"], "premium_access": false, "rate_limit_per_minute": 120, "requests_total": 4182, "status": "active", "expires_at": null, "last_used_at": "2026-10-20T19:51:35.000Z", "created_at": "2026-09-01T08:00:00.000Z" } } ``` Errors: `405 method_not_allowed`. #### POST /keys/rotate Roll the presented key: a fresh secret is generated, the old one stops working on the next request. Shown once, in full, in this response only. ```bash curl -X POST https://milbox.dev/api/v1/keys/rotate \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "id": 7, "label": "reseller-alpha", "key_prefix": "mml_1a2b3c4d", "secret": "mml_<64 hex>", "status": "active", "rotated_at": "2026-10-22T09:14:02.000Z", "warning": "The previous secret stops working immediately. Persist this value now, it is shown once." } } ``` Errors: `409 conflict`, `403 insufficient_scope`. #### GET /plans The plan the key resolves to, the hard limits that plan enforces, and the whole assignable catalog so a client can render an upgrade path without guessing. ```bash curl "https://milbox.dev/api/v1/plans" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "current": { "plan": "premium", "display_name": "Premium", "tagline": "Custom domains, Cloudflare routing, real retention.", "source": "inherited", "expired": false, "limits": { "domains": 10, "addresses": 500, "api_keys": 5, "mail_retention_hours": 168, "rate_limit_per_minute": 120 }, "features": { "custom_domains": true, "premium_domains": true, "cloudflare": true, "catch_all": true } }, "key_limits": { "address_quota": 50, "addresses_used": 12, "addresses_remaining": 38, "message_retention_hours": 72, "rate_limit_per_minute": 120, "premium_access": true }, "catalog": [ { "plan": "free", "display_name": "Free", "tagline": "Read the platform, own nothing.", "limits": { "domains": 0, "addresses": 0, "api_keys": 1, "mail_retention_hours": 24, "rate_limit_per_minute": 30 }, "features": { "custom_domains": false, "premium_domains": false, "cloudflare": false, "catch_all": false } } ] }, "meta": { "plan_registry": "read-only" } } ``` Errors: `401 invalid_api_key`. #### GET /account/requests Per-request audit trail for this key: method, path, status, outcome, IP, and the request id you also receive in the x-request-id header. Filter it to isolate every denied or throttled call. Parameters: - `outcome` (enum, optional): ok | denied_key | denied_ip | denied_expired | rate_limited | maintenance_mode - `status` (integer, optional): Exact HTTP status to match, e.g. 404. - `since` (string, optional): ISO 8601 lower bound on created_at. - `page` (integer, optional): Default 1. - `per_page` (integer, optional): Default 50. Max 200. ```bash curl "https://milbox.dev/api/v1/account/requests?outcome=rate_limited&per_page=20" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "requests": [ { "request_id": "b41f9c7d2e8a1065", "method": "GET", "path": "/api/v1/inbox", "status": 429, "outcome": "rate_limited", "ip": "203.0.113.9", "created_at": "2026-10-22T08:59:11.000Z" } ], "filters": { "outcome": "rate_limited", "status": null, "since": null } }, "meta": { "total": 1, "page": 1, "per_page": 20, "pages": 1 } } ``` ### Addresses Temporary addresses on an active domain. Each key owns the addresses it creates; quota is per key. #### GET /addresses List every address this key owns, newest first, with the quota snapshot in meta. Parameters: - `status` (enum, optional): active | disabled - `domain` (string, optional): Restrict to one domain name. - `q` (string, optional): Substring match on the full address. - `page` (integer, optional): Default 1. - `per_page` (integer, optional): Default 50. Max 100. ```bash curl "https://milbox.dev/api/v1/addresses" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": [ { "id": "k3n8x2p9q1ab", "api_key_id": 4, "domain_id": 12, "local_part": "swift.otter7f", "address": "swift.otter7f@milbox.dev", "label": null, "status": "active", "password_protected": false, "message_count": 3, "last_message_at": "2026-10-20T18:04:11.000Z", "expires_at": "2026-10-27T17:40:00.000Z", "created_at": "2026-10-20T17:40:00.000Z" } ], "meta": { "total": 1, "page": 1, "per_page": 50, "pages": 1, "quota": { "limit": 10, "used": 1, "remaining": 9, "unlimited": false } } } ``` Errors: `422 validation_failed`. #### POST /addresses Create an address. Omit local_part for a random one. Domain must be active with mail enabled. Parameters: - `domain` (string, required): Domain from GET /domains, status active. - `local_part` (string, optional): Custom alias: a-z, 0-9, dot, dash, underscore. 64 chars max. Omit for a random one. - `label` (string, optional): Free-form note, 80 chars max. - `ttl_minutes` (integer, optional): Lifetime in minutes (minimum 1). Omit to inherit the domain mail_cap_hours (null = domain lifetime). ttl_hours is removed. - `password` (string, optional): Protect reads of this address. Max 128 chars. Returned once, plain, in this response only. - `alias_of` (string, optional): Address or public id to alias: shares the group and inbox. ```bash curl -X POST https://milbox.dev/api/v1/addresses \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"domain":"milbox.dev","local_part":"signup","ttl_minutes":30,"password":"s3cret"}' ``` Response: ```json { "ok": true, "data": { "address": { "id": "k3n8x2p9q1ab", "address": "signup@milbox.dev", "status": "active", "expires_at": "2026-10-20T20:10:00.000Z", "purge_after": null, "password_protected": true, "alias_group": "9f2c..." }, "password": "s3cret", "quota": { "limit": 10, "used": 1, "remaining": 9, "unlimited": false } } } ``` Errors: `404 domain_not_found`, `409 domain_not_routable`, `409 address_exists`, `422 ttl_exceeds_domain`, `429 address_quota_exceeded`, `429 plan_address_quota_exceeded`. #### GET /addresses/{id} One address, its alias group, and its newest messages. Accepts the public id or the full email address. Parameters: - `password` (string, optional): Required when the address carries a password (wrong or missing password returns 404). ```bash curl "https://milbox.dev/api/v1/addresses/signup@milbox.dev?password=s3cret" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "address": { "id": "k3n8x2p9q1ab", "address": "signup@milbox.dev", "status": "active" }, "aliases": [{ "id": "k3n8x2p9q1ab", "address": "signup@milbox.dev" }], "messages": { "rows": [], "total": 0, "per_page": 25 } } } ``` Errors: `404 not_found`. #### GET /addresses/{id}/messages The mailbox subresource: every message on one address, with the full /inbox filter grammar still available. Use it when the address is the scope and you do not want to repeat the address on every call. Parameters: - `id` (string, required): Public id, numeric id, or the full address string. - `password` (string, optional): Required when the address carries a password. - `unread` (boolean, optional): Only unread messages. - `starred` (boolean, optional): true for starred only, false to exclude them. - `q` (string, optional): Full-text query on subject and body. - `sort` (enum, optional): newest | oldest | largest - `per_page` (integer, optional): Default 50. Max 100. ```bash curl "https://milbox.dev/api/v1/addresses/alpha@milbox.dev/messages?unread=true&sort=newest" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "address": "alpha@milbox.dev", "messages": [ { "id": "8f2c1d9a4b6e", "address": "alpha@milbox.dev", "from": "billing@vendor.io", "from_name": "Vendor Billing", "subject": "Invoice 2291 overdue", "preview": "Invoice 2291 is 14 days overdue.", "otp": null, "size_bytes": 24811, "attachment_count": 1, "has_html": true, "has_attachments": true, "spam_status": "clean", "priority": null, "received_at": "2026-10-21T11:02:44.000Z", "read_at": null, "starred_at": null, "expires_at": "2026-10-22T11:02:44.000Z", "to": [{ "name": null, "address": "alpha@milbox.dev" }], "cc": [] } ], "filters": { "page": 1, "per_page": 50, "sort": "newest" } }, "meta": { "total": 1, "page": 1, "per_page": 50, "pages": 1, "next_cursor": null } } ``` Errors: `404 not_found`. #### DELETE /addresses/{id} Soft-delete the address and hide its messages from the inbox. purge_after is stamped so the row survives the five minute grace window before the hard delete. ```bash curl -X DELETE "https://milbox.dev/api/v1/addresses/8f2c1d9a4b6e" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "deleted": true, "address": "signup@example.com", "purge_after": "2026-10-21T18:07:44.000Z" } } ``` Errors: `404 not_found`. #### PATCH /addresses/{id} Relabel, disable, re-enable, extend the lifetime, or rotate the password of an address without changing the address string. Extending ttl_minutes also moves purge_after. Parameters: - `label` (string, optional): New label, max 80 chars. null clears it. - `status` (enum, optional): active | disabled. Disabled stops inbound delivery without destroying history. - `ttl_minutes` (integer, optional): Fresh lifetime counted from now, 1 to 44640. Capped by the domain lifetime. - `password` (string, optional): New address password, or null to remove the gate. ```bash curl -X PATCH https://milbox.dev/api/v1/addresses/signup-a1b2 \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"label":"prod-signup","ttl_minutes":1440}' ``` Response: ```json { "ok": true, "data": { "id": "8f2c1d9a4b6e", "address": "signup@example.com", "label": "prod-signup", "status": "active", "expires_at": "2026-10-22T18:02:44.000Z", "purge_after": "2026-10-22T18:07:44.000Z", "message_count": 42 } } ``` Errors: `404 not_found`, `422 ttl_exceeds_domain`. ### Inbox The mailbox router: list, count, mark, star, delete, restore, and index attachments across every address this key owns. Single-message reads and binary downloads live in the Messages group under /messages. #### GET /inbox Newest messages first. Filter by address, sender, subject, To recipient, sender display name, exact Message-ID, header Date window, size range, priority, attachments, spam state, unread, or poll with since_id. Parameters: - `address` (string, optional): Full email address to scope the list. Password-protected addresses return 404 unless the matching password is supplied. - `password` (string, optional): Address password. Required when the scoped address carries one. - `since_id` (string, optional): Incremental poll cursor: the message id returned by a previous call (public id or numeric). Only rows newer than it are returned. - `cursor` (string, optional): Keyset pagination cursor: the meta.next_cursor value from a previous page. Stable while new mail arrives, unlike page offsets. - `unread` (boolean, optional): true returns only unread messages. - `q` (string, optional): Substring search across subject, from name, from address, and the decoded text body. - `from` (string, optional): Exact From address as sent by the sender (RFC 5322 header, decoded, lowercased). - `from_name` (string, optional): Substring match on the decoded From display name. - `subject` (string, optional): Substring match on the decoded Subject. - `to` (string, optional): Match a full recipient address inside the To header (exact, case-insensitive). - `message_id` (string, optional): Exact Message-ID header value, angle brackets included. - `since` (string, optional): ISO-8601 lower bound on the Date header written by the sender (not arrival time). - `until` (string, optional): ISO-8601 upper bound on the sender Date header. - `min_size` (integer, optional): Minimum message size in bytes. - `max_size` (integer, optional): Maximum message size in bytes. - `priority` (enum, optional): high | normal | low (from X-Priority / Importance / Priority headers). - `has_attachments` (boolean, optional): true returns only messages with attachments. - `starred` (boolean, optional): true for starred messages only, false to exclude them. Anything else is rejected with 422. - `ids` (string, optional): Comma separated message identifiers, public or numeric. Max 200 per call. - `spam` (enum, optional): clean | spam | unknown - `sort` (enum, optional): newest (default) | oldest | largest - `include_deleted` (boolean, optional): Include soft-deleted messages. - `page` (integer, optional): Default 1. - `per_page` (integer, optional): Default 25. Max 100. ```bash curl "https://milbox.dev/api/v1/inbox?address=swift.otter7f@milbox.dev&unread=true" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": [ { "id": "8f2c1d9a4b6e", "address": "swift.otter7f@milbox.dev", "from": "noreply@github.com", "from_name": "GitHub", "subject": "Your verification code is 481920", "preview": "Use 481920 to finish signing in. It expires in 10 minutes.", "otp": "481920", "size_bytes": 18422, "attachment_count": 0, "has_html": true, "has_attachments": false, "spam_status": "clean", "priority": null, "received_at": "2026-10-20T18:02:44.000Z", "read_at": null, "starred_at": null, "expires_at": "2026-10-21T18:02:44.000Z", "to": [{ "name": null, "address": "swift.otter7f@milbox.dev" }], "cc": [] } ], "meta": { "total": 1, "page": 1, "per_page": 25, "pages": 1 } } ``` #### POST /inbox/read Bulk mark messages as read. Every inbox filter is honoured, plus an optional ids array to target specific messages. Send no body to act on the whole filtered scope. Returns the affected row count and the refreshed counters. Parameters: - `ids` (array, optional): Public ids or numeric ids, max 500. Omit to apply to every message matching the query filters. - `address` (string, optional): Scope to one owned address. - `password` (string, optional): Address password. Required when the scoped address carries one. - `unread` (boolean, optional): true limits the action to currently unread messages. - `include_deleted` (boolean, optional): Also touch soft-deleted messages. - `q` (string, optional): Substring search across subject, from name, and from address. - `from` (string, optional): Exact From address. - `subject` (string, optional): Substring match on the decoded Subject. - `has_attachments` (boolean, optional): Limit to messages carrying attachments. - `spam` (enum, optional): clean | spam | unknown ```bash curl -X POST "https://milbox.dev/api/v1/inbox/read" \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"ids":["8f2c1d9a4b6e","1c4b0e77aa02"]}' ``` Response: ```json { "ok": true, "data": { "seen": true, "marked": 2, "scoped_ids": 2, "counters": { "total": 42, "unread": 5, "read": 37, "deleted": 3, "attachments": 11 } }, "meta": { "request_id": "e7c87866405f3959", "timestamp": "2026-10-20T19:51:35.267Z" } } ``` Errors: `404 not_found`, `422 validation_failed`, `423 domain_blocked`. #### POST /inbox/unread Bulk mark messages as unread. Identical contract to /inbox/read: same filters, same optional ids array, same response shape. Parameters: - `ids` (array, optional): Public ids or numeric ids, max 500. Omit to apply to every message matching the query filters. - `address` (string, optional): Scope to one owned address. - `password` (string, optional): Address password. Required when the scoped address carries one. ```bash curl -X POST "https://milbox.dev/api/v1/inbox/unread" \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"ids":["8f2c1d9a4b6e"]}' ``` Response: ```json { "ok": true, "data": { "seen": false, "marked": 1, "scoped_ids": 1, "counters": { "total": 42, "unread": 6, "read": 36, "deleted": 3, "attachments": 11 } }, "meta": { "request_id": "e7c87866405f3959", "timestamp": "2026-10-20T19:51:35.267Z" } } ``` Errors: `404 not_found`, `422 validation_failed`, `423 domain_blocked`. #### GET /inbox/counts Counter snapshot for the whole filtered scope: total, unread, read, deleted, attachments, starred. Accepts every inbox filter, so you can scope it per address. Parameters: - `address` (string, optional): Scope the counters to one owned address. - `password` (string, optional): Address password when the mailbox is gated. ```bash curl "https://milbox.dev/api/v1/inbox/counts?address=signup@example.com" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "address": "signup@example.com", "total": 42, "unread": 7, "read": 35, "deleted": 3, "attachments": 5, "starred": 2 }, "meta": { "request_id": "e7c87866405f3959", "timestamp": "2026-10-20T19:51:35.267Z" } } ``` Errors: `404 not_found`, `423 domain_blocked`. #### POST /inbox/star Mark messages as starred. Same body and filter contract as /inbox/read: optional ids array, otherwise the whole filtered scope. Parameters: - `ids` (array, optional): Public ids or numeric ids, max 500. Omit to apply to every message matching the filters. ```bash curl -X POST "https://milbox.dev/api/v1/inbox/star" \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"ids":["8f2c1d9a4b6e"]}' ``` Response: ```json { "ok": true, "data": { "action": "star", "affected": 1, "scoped_ids": 1, "counters": { "total": 42, "unread": 7, "read": 35, "deleted": 3, "attachments": 5, "starred": 1 } } } ``` Errors: `422 validation_failed`. #### POST /inbox/unstar Remove the star flag from messages. Identical contract to /inbox/star. Parameters: - `ids` (array, optional): Public ids or numeric ids, max 500. ```bash curl -X POST "https://milbox.dev/api/v1/inbox/unstar" \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"ids":["8f2c1d9a4b6e"]}' ``` Response: ```json { "ok": true, "data": { "action": "unstar", "affected": 1, "scoped_ids": 1, "counters": { "total": 42, "unread": 7, "read": 35, "deleted": 3, "attachments": 5, "starred": 0 } } } ``` Errors: `422 validation_failed`. #### POST /inbox/delete Bulk soft delete every message matching the filters or the supplied ids, up to 5000 rows in one call. message_count is recalculated for each touched address. Parameters: - `ids` (array, optional): Public ids or numeric ids, max 500. ```bash curl -X POST "https://milbox.dev/api/v1/inbox/delete?address=signup@example.com" \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"ids":["8f2c1d9a4b6e","3c7e0f1a2b9d"]}' ``` Response: ```json { "ok": true, "data": { "action": "delete", "affected": 12, "scoped_ids": null, "counters": { "total": 30, "unread": 4, "read": 26, "deleted": 15, "attachments": 2, "starred": 0 } } } ``` Errors: `422 validation_failed`. #### POST /inbox/restore Undo a soft delete. Restores the messages into the inbox and refreshes the per-address message_count. Parameters: - `ids` (array, optional): Public ids or numeric ids, max 500. ```bash curl -X POST "https://milbox.dev/api/v1/inbox/restore" \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"ids":["8f2c1d9a4b6e"]}' ``` Response: ```json { "ok": true, "data": { "action": "restore", "affected": 1, "scoped_ids": 1, "counters": { "total": 43, "unread": 7, "read": 36, "deleted": 2, "attachments": 5, "starred": 1 } } } ``` Errors: `422 validation_failed`. #### GET /inbox/attachments Flat attachment index across every message this key can read, newest first. One page of messages in, every attachment on those messages out, each with checksum_sha256 and a ready-to-call download_path pointing at /messages/{id}/attachment/{attachmentId}. Parameters: - `address` (string, optional): Scope to one owned address. - `page` (integer, optional): Default 1. - `per_page` (integer, optional): Default 25. Max 100. ```bash curl "https://milbox.dev/api/v1/inbox/attachments?address=signup@example.com&per_page=25" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": [ { "message_id": "8f2c1d9a4b6e", "address": "signup@example.com", "subject": "Your invoice", "from": "billing@vendor.example", "filename": "invoice-2291.pdf", "content_type": "application/pdf", "size": 184233, "checksum_sha256": "9f2b...", "download_path": "/api/v1/messages/8f2c1d9a4b6e/attachment/9f2b1c4d" } ], "meta": { "total": 1, "page": 1, "per_page": 25, "pages": 1 } } ``` ### Messages The single-message router: read one message with full RFC 5322 fidelity, patch its flags, soft-delete it, download the raw .eml, or download one attachment. The attachment index that produces these ids lives at GET /inbox/attachments in the Inbox group. #### GET /messages/{id} The full message: envelope-level sender and recipient, header Date with a validity flag, In-Reply-To, References, priority, Received chain, DKIM signatures, Authentication-Results (joined into one string), attachment checksums, raw_available, and the decoded text and html bodies. Marks it read. Password-protected addresses require the password query param. Parameters: - `id` (string, required): Public id or numeric internal id. - `password` (string, optional): Required when the owning address carries a password (wrong or missing password returns 404). ```bash curl "https://milbox.dev/api/v1/messages/8f2c1d9a4b6e" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "id": "8f2c1d9a4b6e", "address": "signup@milbox.dev", "from": "noreply@vendor.example", "from_name": "Vendor", "subject": "Your verification code is 481920", "preview": "Use 481920 to finish signing in.", "otp": "481920", "size_bytes": 18422, "attachment_count": 1, "has_html": true, "has_attachments": true, "spam_status": "clean", "priority": "normal", "received_at": "2026-10-20T18:02:44.000Z", "read_at": "2026-10-20T18:03:10.000Z", "starred_at": null, "expires_at": "2026-10-21T18:02:44.000Z", "to": [{ "name": "Signup", "address": "signup@milbox.dev" }], "cc": [], "reply_to": null, "message_id": "", "in_reply_to": null, "references": [], "headers_date": "2026-10-20T18:02:40.000Z", "headers_date_valid": true, "received_chain": ["from mail.vendor.example by mx.milbox.dev"], "dkim_signatures": ["v=1; a=rsa-sha256; d=vendor.example; s=sel"], "authentication_results": "mx.milbox.dev; spf=pass; dkim=pass; dmarc=pass", "raw_available": true, "text": "Use 481920 to finish signing in.", "html": "

Use 481920 to finish signing in.

", "attachments": [ { "filename": "invoice-2291.pdf", "content_type": "application/pdf", "size": 184233 } ], "attachments_detail": [ { "filename": "invoice-2291.pdf", "content_type": "application/pdf", "size": 184233, "content_id": null, "disposition": "attachment", "inline": false, "checksum_sha256": "9f2b1c4d...", "related": false } ], "delivery_ms": 84 }, "meta": { "request_id": "e7c87866405f3959", "timestamp": "2026-10-20T19:51:35.267Z" } } ``` Errors: `404 not_found`. #### PATCH /messages/{id} Update the read flag or restore a soft-deleted message. Send seen and/or is_deleted; send neither and the call is rejected with 422. Parameters: - `id` (string, required): Public id or numeric internal id. - `seen` (boolean, optional): true sets read_at, false clears it. - `is_deleted` (boolean, optional): true soft-deletes, false restores. - `password` (string, optional): Required when the owning address carries a password (wrong or missing password returns 404). ```bash curl -X PATCH "https://milbox.dev/api/v1/messages/8f2c1d9a4b6e" \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"seen":true}' ``` Response: ```json { "ok": true, "data": { "id": "8f2c1d9a4b6e", "seen": true, "is_deleted": false, "read_at": "2026-10-21T00:12:04.000Z" } } ``` Errors: `404 not_found`, `422 validation_failed`. #### GET /messages/{id}/download Download the stored message as raw RFC 822 bytes (message/rfc822, attachment disposition). Parameters: - `id` (string, required): Public id or numeric internal id. - `password` (string, optional): Required when the owning address carries a password (wrong or missing password returns 404). ```bash curl -OJ "https://milbox.dev/api/v1/messages/8f2c1d9a4b6e/download" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json Downloaded file: `your-verification-code-is-481920.eml`, content-type `message/rfc822`, disposition `attachment`. ``` Errors: `404 not_found`. #### GET /messages/{id}/attachment/{attachmentId} Fetch one attachment by its SHA-256 checksum (full or prefix), its zero-based index, or its att_N label. Content-type and filename come from the MIME part. Parameters: - `id` (string, required): Public id or numeric internal id. - `attachmentId` (string, required): checksum_sha256 from attachments_detail (full or prefix), zero-based index, or att_N (zero-based). - `password` (string, optional): Required when the owning address carries a password (wrong or missing password returns 404). ```bash curl -OJ "https://milbox.dev/api/v1/messages/8f2c1d9a4b6e/attachment/3f9a" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json Downloaded file: `invoice-2291.pdf`, content-type `application/pdf` taken from the MIME part, disposition `attachment`, header `x-content-type-options: nosniff`. ``` Errors: `404 not_found`. #### DELETE /messages/{id} Soft-delete one message. Parameters: - `id` (string, required): Public id or numeric internal id. - `password` (string, optional): Required when the owning address carries a password (wrong or missing password returns 404). ```bash curl -X DELETE "https://milbox.dev/api/v1/messages/8f2c1d9a4b6e" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "deleted": 1, "addresses": 1 } } ``` Errors: `404 not_found`. #### POST /messages/bulk One call for the whole sweep: delete, read, unread, star or unstar every message that matches an address, a filter, or an explicit id list. Caps at 500 rows per call on purpose so a runaway sweep cannot take the mailbox with it. Parameters: - `action` (enum, optional): delete | read | unread | star | unstar. Defaults to delete; may also be sent as the action query parameter. - `ids` (array, optional): Public ids or numeric ids. Max 500 entries. - `address` (string, optional): Restrict the sweep to one mailbox. - `filter` (enum, optional): unread | spam | not_spam ```bash curl -X POST "https://milbox.dev/api/v1/messages/bulk" \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"action":"read","address":"alpha@milbox.dev","filter":"unread"}' ``` Response: ```json { "ok": true, "data": { "action": "read", "affected": 37, "scoped_ids": null } } ``` Errors: `422 validation_failed`, `404 not_found`. #### GET /messages/search Full-text search across subject and body for the mail this key owns. Same filter grammar as /inbox, prefix q with +term to require a word and -term to exclude it, quote a phrase to match it literally. Parameters: - `q` (string, required): Full-text query. +required -excluded "exact phrase". - `address` (string, optional): Restrict to one mailbox. - `since` (string, optional): ISO 8601 lower bound on received_at. - `per_page` (integer, optional): Default 50. Max 100. ```bash curl "https://milbox.dev/api/v1/messages/search?q=invoice+overdue&address=alpha@milbox.dev" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": [ { "id": "8f2c1d9a4b6e", "address": "alpha@milbox.dev", "from": "billing@vendor.io", "from_name": "Vendor Billing", "subject": "Invoice 2291 overdue", "preview": "Invoice 2291 is 14 days overdue.", "otp": null, "size_bytes": 24811, "attachment_count": 1, "has_html": true, "has_attachments": true, "spam_status": "clean", "priority": null, "received_at": "2026-10-21T11:02:44.000Z", "read_at": null, "starred_at": null, "expires_at": "2026-10-22T11:02:44.000Z", "to": [{ "name": null, "address": "alpha@milbox.dev" }], "cc": [] } ], "meta": { "total": 1, "q": "invoice overdue", "page": 1, "per_page": 25, "next_cursor": null, "searched": ["subject", "from_address", "from_name", "text_body"] } } ``` Errors: `422 validation_failed`. ### Domains The domain inventory for the account behind the key. Premium rows are hidden unless the key carries premium_access. #### GET /domains Two views in one route: with no key ownership rows this lists the public catalog; domains this key registered always appear here too. Filters cover status, type, and mail routing state. Parameters: - `q` (string, optional): Substring match on the domain name. - `type` (enum, optional): platform_pool | custom - `status` (enum, optional): active | verified - `premium` (boolean, optional): true | false - `page` (integer, optional): Default 1. - `per_page` (integer, optional): Default 50. Max 200. ```bash curl "https://milbox.dev/api/v1/domains?type=platform_pool&per_page=100" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": [ { "id": "17717649", "name": "alpha.milbox.dev", "type": "platform_pool", "premium": false, "status": "active", "verified_at": "2026-10-01T08:12:00.000Z", "mx_target": "mx.milbox.dev", "mail_mode": "mx_txt", "added_at": "2026-09-30T22:01:44.000Z" } ], "meta": { "total": 128, "page": 1, "per_page": 100, "pages": 2 } } ``` Errors: `422 validation_failed`. #### GET /domains/{id} Fetch one domain by public id or by name. One call, no list scan. ```bash curl https://milbox.dev/api/v1/domains/alpha.milbox.dev \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "id": "17717649", "name": "alpha.milbox.dev", "type": "platform_pool", "premium": false, "status": "active", "verified_at": "2026-10-01T08:12:00.000Z", "mx_target": "mx.milbox.dev", "mail_mode": "mx_txt", "added_at": "2026-09-30T22:01:44.000Z" } } ``` Errors: `404 not_found`, `403 premium_gated`. #### GET /domains/check Ask whether a domain is registerable before the user commits to it. Parameters: - `domain` (string, required): Candidate domain, normalised server-side. ```bash curl "https://milbox.dev/api/v1/domains/check?domain=example.com" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "domain": "example.com", "available": false, "reason": "registered", "premium": false, "status": "active" } } ``` Errors: `422 validation_failed`. #### POST /domains Register a custom domain for temp mail. Returns the TXT challenge to publish before verifying. Parameters: - `domain` (string, required): Body field. Domain to register, normalised server-side. ```bash curl -X POST "https://milbox.dev/api/v1/domains" \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"domain":"example.com"}' ``` Response: ```json { "ok": true, "data": { "domain": { "id": "46547839", "name": "example.com", "type": "custom", "premium": false, "status": "pending_verification", "mail_enabled": true, "verification": { "host": "_milbox-challenge.example.com", "value": "milbox-verify=", "expires_at": "2026-10-23T18:38:58.000Z", "pending": true } }, "provisioning": { "provider": "cloudflare", "applied": true, "skipped_reason": null, "txt_created": true, "mx_created": false, "mx_skipped_reason": "mx_present", "zone": "", "error": null } } } ``` Errors: `403 plan_custom_domains_disabled`, `429 domain_quota_exceeded`, `409 domain_taken`, `409 domain_exists`, `422 validation_failed`. #### PATCH /domains/{id} Update notes, toggle mail routing, or set mail_cap_hours. mail_enabled only flips while the domain is active. Parameters: - `notes` (string | null, optional): Body field. Free text, max 500 chars, null clears. - `mail_enabled` (boolean, optional): Body field. Remove from or restore to the MX routing table. - `mail_cap_hours` (integer | null, optional): Body field. Max address TTL in hours for addresses created on this domain; null = domain lifetime. ```bash curl -X PATCH "https://milbox.dev/api/v1/domains/46547839" \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"mail_cap_hours":24}' ``` Response: ```json { "ok": true, "data": { "id": "46547839", "name": "example.com", "status": "active", "mail_enabled": true, "mail_cap_hours": 24 } } ``` Errors: `404 domain_not_found`, `409 domain_not_active`. #### DELETE /domains/{id} Soft-delete your domain and pull it from the MX routing table instantly. ```bash curl -X DELETE "https://milbox.dev/api/v1/domains/46547839" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "before": "verified", "deleted": true } } ``` Errors: `404 domain_not_found`. #### POST /domains/{id}/verify Run the TXT + MX check and flip pending_verification to verified on success. Parameters: - `token` (string, optional): Body field. 32 hex chars; omit to use the stored token. ```bash curl -X POST "https://milbox.dev/api/v1/domains/46547839/verify" \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{}' ``` Response: ```json { "ok": true, "data": { "domain": { "id": "46547839", "status": "verified" }, "check": { "txt_ok": true, "mx_ok": true, "mx_found": ["mx.milbox.dev"] }, "verified": true } } ``` Errors: `404 domain_not_found`, `409 verify_failed`. #### POST /domains/{id}/rotate Issue a fresh 72h TXT challenge token; resets verification state. ```bash curl -X POST "https://milbox.dev/api/v1/domains/46547839/rotate" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "domain": { "id": "46547839", "status": "pending_verification" }, "verification": { "value": "milbox-verify=", "expires_at": "2026-10-23T18:38:58.000Z", "pending": true } } } ``` Errors: `404 domain_not_found`. #### GET /domains/{id}/check Live DNS probe without state changes; writes last_check_result for the detail view. ```bash curl "https://milbox.dev/api/v1/domains/46547839/check" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "domain": { "id": "46547839", "status": "pending_verification" }, "check": { "txt_ok": false, "txt_found": [], "mx_ok": false, "mx_found": [], "error": null } } } ``` Errors: `404 domain_not_found`. #### POST /domains/{id}/transition Lifecycle moves plus moderation: activate, resume, block for spam, fraud, abuse, or phishing, and lift the block. Parameters: - `action` (enum, required): Body field. activate | resume | block | spam | fraud | abuse | phishing | lift - `note` (string, optional): Body field. Free text stored on the moderation row and shown to the owner. ```bash curl -X POST "https://milbox.dev/api/v1/domains/46547839/transition" \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"action":"spam","note":"Bulk unsolicited mail reported by three recipients."}' ``` Response: ```json { "ok": true, "data": { "domain": { "id": "46547839", "status": "suspended", "moderation": { "blocked": true, "reason": "spam", "note": "Bulk unsolicited mail reported by three recipients.", "blocked_at": "2026-10-21T04:11:02.000Z", "restores_to": "active" } }, "moderation": { "action": "spam", "label": "Spam blocked", "reason": "spam", "blocking": true, "before": "active", "after": "suspended", "note": "Bulk unsolicited mail reported by three recipients.", "explanation": "Mail rejected with 550 domain blocked for spam." } } } ``` Errors: `404 domain_not_found`, `409 invalid_transition`, `423 domain_blocked`. #### GET /domains/pool Per-domain counters for this key only: how many addresses live on each routable domain and how many live messages it holds. Answers "which of my domains is actually carrying traffic" without paging the inbox. ```bash curl "https://milbox.dev/api/v1/domains/pool" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": [ { "domain": "inboxmil.xyz", "type": "platform_pool", "premium": false, "addresses": 4, "messages": 38 } ] } ``` ### Webhooks Push instead of poll. Register an HTTPS endpoint, pick events, and every delivery carries an HMAC-SHA256 signature over `${timestamp}.${body}` in the x-milbox-signature header. Failed deliveries retry twice with a 5s then 10s backoff and the per-hook failure counter resets on the next success. #### GET /webhooks List the webhooks registered for this key, newest first. The signing secret is returned once at creation and afterwards only as a stored value on this endpoint. ```bash curl "https://milbox.dev/api/v1/webhooks" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": [ { "public_id": "39396a892a7c2277", "url": "https://example.com/hooks/milbox", "description": "prod ingest", "events": "message.received,address.created", "secret": "whsec_9a1c...", "active": true, "failure_count": 0, "last_status": 200, "last_error": null, "last_delivery_at": "2026-10-20T19:51:35.000Z", "created_at": "2026-10-20T19:40:00.000Z", "updated_at": "2026-10-20T19:51:35.000Z" } ] } ``` #### POST /webhooks Register a webhook. Ten active hooks per key. Honours the idempotency-key header so a retried POST does not double-register. Parameters: - `url` (string, required): HTTPS endpoint that accepts a JSON POST. Must be a valid absolute URL. - `description` (string, optional): Free-form label, max 190 chars. - `events` (array, optional): message.received | address.created | address.expired | message.deleted. Defaults to message.received. ```bash curl -X POST https://milbox.dev/api/v1/webhooks \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"url":"https://example.com/hooks/milbox","events":["message.received"]}' ``` Response: ```json { "ok": true, "data": { "public_id": "39396a892a7c2277", "url": "https://example.com/hooks/milbox", "events": "message.received", "secret": "whsec_9a1c4f...", "active": true, "failure_count": 0 } } ``` Errors: `422 validation_failed`, `429 webhook_limit_reached`. #### GET /webhooks/{id} One webhook plus its recent delivery log: event, HTTP status, duration, attempts, error. Use it to debug a receiver that started refusing payloads. Parameters: - `limit` (integer, optional): Deliveries to embed, default 50, max 200. ```bash curl "https://milbox.dev/api/v1/webhooks/39396a892a7c2277?limit=20" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "public_id": "39396a892a7c2277", "url": "https://example.com/hooks/milbox", "active": true, "deliveries": [ { "event": "message.received", "status": 200, "duration_ms": 84, "attempts": 1, "error": null, "delivered_at": "2026-10-20T19:51:35.000Z" } ] } } ``` Errors: `404 not_found`. #### PATCH /webhooks/{id} Change the target URL, description, event list, or flip active on and off. Pausing keeps the delivery log. Parameters: - `url` (string, optional): New absolute URL. - `description` (string, optional): New label, or null to clear. - `events` (array, optional): Replacement event list. - `active` (boolean, optional): false pauses deliveries without deleting the hook. ```bash curl -X PATCH "https://milbox.dev/api/v1/webhooks/39396a892a7c2277" \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"active":false}' ``` Response: ```json { "ok": true, "data": { "public_id": "39396a892a7c2277", "url": "https://example.com/hooks/milbox", "active": false, "failure_count": 0 } } ``` Errors: `404 not_found`, `422 validation_failed`. #### DELETE /webhooks/{id} Delete the webhook and its delivery log. Irreversible; pause with PATCH active=false when the receiver is only temporarily broken. ```bash curl -X DELETE "https://milbox.dev/api/v1/webhooks/39396a892a7c2277" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "deleted": true, "public_id": "39396a892a7c2277" } } ``` Errors: `404 not_found`. #### POST /webhooks/{id}/test Send a signed webhook.test delivery right now and get the receiver status back synchronously. The fastest way to prove the signature check on your end before a real mail arrives. Parameters: - `id` (string, required): Webhook public id. ```bash curl -X POST "https://milbox.dev/api/v1/webhooks/39396a892a7c2277/test" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "webhook": "39396a892a7c2277", "url": "https://example.com/hooks/milbox", "delivered": true, "status": 200, "duration_ms": 184, "error": null } } ``` Errors: `404 not_found`. ### Sender rules Inbound allow and block rules evaluated during ingest, before a message is written. A blocked sender is rejected with the SMTP code sender_blocked and never appears in the inbox. #### GET /sender-rules The inbound allow and block list for this key, with counts per kind. Rules are evaluated before a message is stored, so a blocked sender never lands in the mailbox at all. ```bash curl "https://milbox.dev/api/v1/sender-rules" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "rules": [ { "id": 12, "pattern": "*@promo.example", "kind": "block", "created_at": "2026-10-21T09:12:00.000Z" }, { "id": 9, "pattern": "@trusted.partner", "kind": "allow", "created_at": "2026-10-20T14:02:11.000Z" } ], "counts": { "allow": 1, "block": 1 } } } ``` #### POST /sender-rules Add a rule, or flip the kind on an existing pattern. Once a key carries at least one allow rule the list becomes a real allowlist: any sender that matches nothing is treated as blocked. Parameters: - `pattern` (string, required): address, domain, @domain, *@domain, or a glob such as *@*.promo.example - `kind` (enum, optional): allow | block. Defaults to block. ```bash curl -X POST "https://milbox.dev/api/v1/sender-rules" \ -H "x-api-key: $MILBOX_API_KEY" \ -H "content-type: application/json" \ -d '{"pattern":"*@promo.example","kind":"block"}' ``` Response: ```json { "ok": true, "data": { "rule": { "id": 13, "pattern": "*@promo.example", "kind": "block", "created_at": "2026-10-22T01:04:09.000Z" } } } ``` Errors: `409 conflict`, `422 validation_failed`. #### GET /sender-rules/{id} One rule by numeric id. Parameters: - `id` (integer, required): Rule id, integers only. ```bash curl "https://milbox.dev/api/v1/sender-rules/13" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "rule": { "id": 13, "pattern": "*@promo.example", "kind": "block", "created_at": "2026-10-22T01:04:09.000Z" } } } ``` Errors: `404 not_found`. #### DELETE /sender-rules/{id} Remove a rule. Mail from that sender flows normally on the next ingest. Parameters: - `id` (integer, required): Rule id, integers only. ```bash curl -X DELETE "https://milbox.dev/api/v1/sender-rules/13" \ -H "x-api-key: $MILBOX_API_KEY" ``` Response: ```json { "ok": true, "data": { "deleted": true, "id": 13 } } ``` Errors: `404 not_found`. ## Availability reasons - `available`: Nothing registered under that name. Safe to offer. - `registered`: A live row exists. Do not offer it again. - `premium_gated`: The row is premium and this key cannot see it. - `unsupported`: The row exists but is pending or deleted. - `blocked`: An operator blocked the row for spam, fraud, abuse, or phishing. moderation.reason carries which. ## Error codes - `400 bad_request`: The identifier in the path is malformed. - `401 invalid_api_key`: The presented key does not match any stored hash. - `401 invalid_key`: See the reference documentation. - `401 key_expired`: The key passed its expiry timestamp. - `401 key_revoked`: The key was revoked from the panel. - `401 missing_api_key`: No x-api-key header and no Bearer token. - `401 no_session`: See the reference documentation. - `401 unauthorized`: No active panel session (panel API surface only). - `403 insufficient_scope`: See the reference documentation. - `403 ip_not_allowed`: Caller IP is outside the key allowlist. error.details.ip_allowlist echoes the expected ranges. - `403 owner_only`: Only the owner role may rewrite plan limits. - `403 plan_cloudflare_disabled`: The plan does not include Cloudflare routing. - `403 plan_custom_domains_disabled`: The plan does not include custom domains. - `403 plan_premium_domains_disabled`: The plan does not include premium domains. - `403 plan_subject_missing`: No owning account could be resolved for the key. - `403 premium_gated`: Row requires premium access the key does not carry. - `404 alias_target_not_found`: See the reference documentation. - `404 domain_not_found`: No domain owned by this key matches the identifier. - `404 not_found`: No visible domain, address, or message matches the identifier. - `405 method_not_allowed`: See the reference documentation. - `409 address_exists`: That local part is already taken on the domain. - `409 conflict`: The resource could not be deleted in its current state. - `409 domain_abuse`: The domain is blocked for abuse. Inbound mail is rejected with 550. - `409 domain_ban`: The domain is banned by an operator. Inbound mail is rejected with 550. - `409 domain_exists`: This domain is already registered under your key. - `409 domain_fraud`: The domain is blocked for fraud. Inbound mail is rejected with 550. - `409 domain_not_active`: This operation requires the domain to be in the active state. - `409 domain_not_routable`: Domain exists but is not active or has mail disabled. - `409 domain_phishing`: The domain is blocked for phishing. Inbound mail is rejected with 550. - `409 domain_spam`: The domain is blocked for spam. Inbound mail is rejected with 550. - `409 domain_suspend`: The domain is suspended by an operator. Inbound mail is rejected with 550. - `409 domain_taken`: This domain is already registered on the platform. - `409 email_taken`: See the reference documentation. - `409 invalid_transition`: The requested lifecycle transition is not valid for the current domain status. - `409 verify_failed`: No verification token exists. Rotate the token first, then retry. - `413 message_too_large`: See the reference documentation. - `415 unsupported_media_type`: See the reference documentation. - `422 allowlist_required`: See the reference documentation. - `422 current_session`: See the reference documentation. - `422 empty_message`: See the reference documentation. - `422 empty_patch`: See the reference documentation. - `422 invalid_ip_entry`: See the reference documentation. - `422 no_allowed_settings`: See the reference documentation. - `422 reserved_local_part`: postmaster, abuse, admin and friends are reserved. - `422 sender_blocked`: Inbound mail from that sender matched a block rule on the key, or matched no allow rule while an allowlist exists. Rejected during ingest, before storage. - `422 status_not_patchable`: See the reference documentation. - `422 token_invalid`: The supplied verification token is not 32 hexadecimal characters. - `422 ttl_exceeds_domain`: See the reference documentation. - `422 validation_failed`: A field or query parameter is outside the accepted set. error.details lists each offending field. - `423 domain_blocked`: An operator blocked the domain. 423 on mailbox creation, inbox, address, and message reads; lifecycle moves return 409. Lift the block first. - `429 address_quota_exceeded`: Per-key address quota reached. meta.quota shows the limit. - `429 api_key_quota_exceeded`: Active API-key ceiling for the plan reached. - `429 domain_quota_exceeded`: Account domain ceiling for the plan reached. error.details carries plan, limit, used, remaining. - `429 plan_address_quota_exceeded`: Account-wide mailbox ceiling for the plan reached. error.details carries plan, limit, used, remaining. - `429 rate_limited`: Per-key budget exhausted. error.details.retry_after holds the wait in seconds; the response also carries the standard Retry-After header. - `429 webhook_limit_reached`: See the reference documentation. - `500 internal_error`: An unexpected server condition. Retry with backoff; meta.request_id identifies the request in support inquiries. - `500 settings_write_failed`: See the reference documentation. - `503 ingest_disabled`: See the reference documentation. - `503 maintenance_mode`: The public API is paused from the panel maintenance switch. Panel and ingest are unaffected; 503 carries a retry-after header.