# ApiMail — Developer Guide ApiMail is an API-first email platform. Send and receive email programmatically via a REST API — no IMAP, no POP3, no mailbox UI. Create an inbox in one API call on the shared domain, or bring your own domain for full control. ## Base URL ``` https://api.apimail.cc/v1 ``` - Interactive API docs (Swagger UI): https://api.apimail.cc/docs - OpenAPI spec (machine-readable): https://api.apimail.cc/openapi.json The `/docs` page has full request/response schemas, field descriptions, and a "Try it out" feature for every endpoint. Use `/openapi.json` for programmatic consumption. All API endpoints are under the `/v1` prefix. System endpoints (`/health`, `/skill.md`, `/docs`) are at the root. ## Authentication All requests require an `X-API-Key` header: ``` X-API-Key: om_abc123... ``` API keys are returned **once** on creation and **cannot be retrieved again**. Keys have scopes: - `read` — read-only access to messages, threads, inboxes, domains - `send` — send and reply to email - `admin` — full access including creating/deleting resources, managing API keys (implies `send`) Accounts are provisioned by the platform admin — there is no self-service signup. Once you have an API key, you manage your own domains, inboxes, and additional API keys. ## Quick Start ### 1. Set your API key You'll receive an admin API key from the platform admin. Set it as a variable — you'll use it in every request: ```bash export API_KEY="om_your_key_here" ``` ### 2. Create an inbox **Fastest way — shared domain (no DNS setup needed):** ```bash curl -s -X POST https://api.apimail.cc/v1/inboxes/ \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` Response: ```json { "id": "inbox-uuid", "email": "swiftfox482@agentcourier.cc", "local_part": "swiftfox482", "status": "active", "created_at": "2026-03-05T..." } ``` That's it — you have a working inbox. Any email sent to this address is ingested and available via the API. You can also send and reply from shared domain inboxes (up to 25 sends/day, 5 inboxes/account). For higher limits, add a custom domain. **Custom domain — full control:** To use your own domain (e.g. `hello@yourdomain.com`), add and verify it first: ```bash # Add domain curl -s -X POST https://api.apimail.cc/v1/domains/ \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "example.com"}' # Add the DNS records from the response, then verify curl -s -X POST https://api.apimail.cc/v1/domains/$DOMAIN_ID/verify \ -H "X-API-Key: $API_KEY" # Create inbox on your domain curl -s -X POST https://api.apimail.cc/v1/inboxes/ \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "example.com", "local_part": "hello"}' ``` ### 3. Read your mail Inbox endpoints accept either a **UUID** or an **email address** — no need to remember IDs: ```bash # List messages (using email address or inbox UUID) curl -s https://api.apimail.cc/v1/inboxes/swiftfox482@agentcourier.cc/messages \ -H "X-API-Key: $API_KEY" # Get a single message with attachments curl -s https://api.apimail.cc/v1/messages/$MESSAGE_ID \ -H "X-API-Key: $API_KEY" # Fetch and mark as read in one call curl -s "https://api.apimail.cc/v1/messages/$MESSAGE_ID?mark_read=true" \ -H "X-API-Key: $API_KEY" # List only unread messages curl -s "https://api.apimail.cc/v1/inboxes/$INBOX_ID/messages?unread=true" \ -H "X-API-Key: $API_KEY" ``` ### Managing API keys To create additional keys (e.g. a read-only key for a dashboard), first get your account ID: ```bash curl -s https://api.apimail.cc/v1/accounts/me -H "X-API-Key: $API_KEY" ``` Then create a key: ```bash curl -s -X POST https://api.apimail.cc/v1/accounts/$ACCOUNT_ID/api-keys \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{"label": "read-only-dashboard", "scopes": ["read"]}' ``` ## Complete API Reference ### Account & API Keys | Method | Path | Scope | Description | |--------|------|-------|-------------| | GET | `/v1/accounts/me` | read | Get your account info (ID, email, plan). | | GET | `/v1/accounts/{id}/api-keys` | admin | List API keys (shows prefix, label, scopes — never the raw key). | | POST | `/v1/accounts/{id}/api-keys` | admin | Create a new API key. Raw key returned only in this response. | | DELETE | `/v1/accounts/{id}/api-keys/{key_id}` | admin | Revoke an API key (irreversible). | ### Domains | Method | Path | Scope | Description | |--------|------|-------|-------------| | POST | `/v1/domains/` | admin | Add a domain. Returns DNS records to configure. | | GET | `/v1/domains/` | read | List all domains. | | GET | `/v1/domains/{id}` | read | Get a single domain with verification status. | | POST | `/v1/domains/{id}/verify` | admin | Check all DNS records (MX, ownership, SPF, DKIM, DMARC) and activate domain. Sets `send_enabled` when all 5 pass. | | POST | `/v1/domains/{id}/setup-dkim` | admin | Generate DKIM keypair. Returns the DNS TXT record to add. | | DELETE | `/v1/domains/{id}` | admin | Delete a domain and all its inboxes. | ### Inboxes Inbox endpoints accept either a **UUID** or an **email address** as the identifier. For example, `/v1/inboxes/hello@example.com` and `/v1/inboxes/550e8400-...` both work. | Method | Path | Scope | Description | |--------|------|-------|-------------| | POST | `/v1/inboxes/` | admin | Create an inbox. Omit domain for shared domain (auto-generated address), or provide `domain`/`domain_id` for custom domain. Optional `display_name` sets the From header name. | | GET | `/v1/inboxes/` | read | List all inboxes. | | GET | `/v1/inboxes/{email_or_id}` | read | Get a single inbox by email address or UUID. | | PATCH | `/v1/inboxes/{email_or_id}` | admin | Update inbox properties (e.g. `status`, `display_name`). | | DELETE | `/v1/inboxes/{email_or_id}` | admin | Deactivate an inbox (sets status to inactive). | | GET | `/v1/inboxes/{email_or_id}/messages` | read | List messages in an inbox (paginated, searchable). | **Creating an inbox:** ```bash # Shared domain — no setup, auto-generated address (25 sends/day, limit 5 inboxes/account) curl -s -X POST https://api.apimail.cc/v1/inboxes/ \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{}' # Custom domain — using domain name curl -s -X POST https://api.apimail.cc/v1/inboxes/ \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "example.com", "local_part": "hello"}' # Custom domain — using domain UUID + display name curl -s -X POST https://api.apimail.cc/v1/inboxes/ \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain_id": "'$DOMAIN_ID'", "local_part": "hello", "display_name": "Hello Support"}' ``` **Message listing query params:** | Param | Type | Default | Description | |-------|------|---------|-------------| | `cursor` | string | — | Pagination cursor from previous response's `next_cursor` | | `limit` | int | 20 | Results per page (1-100) | | `q` | string | — | Full-text search across subject, sender, and body | | `unread` | bool | — | `true` for unread only, `false` for read only, omit for all | | `label` | string | — | Filter by label(s). Repeat for multiple: `?label=billing&label=urgent`. Messages must have ALL specified labels. | ### Messages | Method | Path | Scope | Description | |--------|------|-------|-------------| | GET | `/v1/messages/{id}` | read | Get a message with attachments. Add `?mark_read=true` to mark read. | | PATCH | `/v1/messages/{id}` | read | Update message properties (e.g. `{"labels": ["processed", "billing"]}`). | | GET | `/v1/messages/{id}/raw` | read | Download the original .eml file (RFC 2822 format). | | GET | `/v1/messages/{id}/attachments/{att_id}` | read | Download an attachment. | | POST | `/v1/messages/{id}/read` | read | Mark a message as read. | | POST | `/v1/messages/{id}/unread` | read | Mark a message as unread. | | DELETE | `/v1/messages/{id}` | admin | Soft-delete a message (hidden, not permanently removed). | ### Threads Threads group related messages based on email In-Reply-To and References headers. | Method | Path | Scope | Description | |--------|------|-------|-------------| | GET | `/v1/threads` | read | List threads (paginated, newest activity first). | | GET | `/v1/threads/{id}/messages` | read | Get all messages in a thread (oldest first). | | DELETE | `/v1/threads/{id}` | admin | Soft-delete all messages in a thread. | **Thread listing query params:** | Param | Type | Default | Description | |-------|------|---------|-------------| | `cursor` | string | — | Pagination cursor | | `limit` | int | 20 | Results per page (1-100) | | `inbox_id` | UUID | — | Filter threads by inbox UUID | | `inbox` | string | — | Filter threads by inbox email address (e.g. `hello@example.com`). Alternative to `inbox_id`. | ### Sending Email Send outbound email from any inbox whose domain is `send_enabled`. Requires `send` or `admin` scope. | Method | Path | Scope | Description | |--------|------|-------|-------------| | POST | `/v1/messages/send` | send | Send a new email. | | POST | `/v1/messages/reply` | send | Reply to an existing message. Auto-sets In-Reply-To, References, and Re: subject. | | POST | `/v1/messages/reply-all` | send | Reply to all recipients. Auto-derives To (original sender) and CC (all other recipients minus your address). | | POST | `/v1/messages/forward` | send | Forward a message to new recipients. Includes original body as quoted content, sets Fwd: subject. | | GET | `/v1/messages/send-jobs` | send | List send jobs (filterable by `?status=sent/failed/queued`). | | GET | `/v1/messages/send-jobs/{id}` | send | Get a single send job's status. | #### Sending a new email You can specify the sending inbox by email address (`"from"`) or UUID (`"from_inbox_id"`): ```bash # Using email address (recommended) curl -s -X POST https://api.apimail.cc/v1/messages/send \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "hello@example.com", "to": ["recipient@example.com"], "subject": "Hello from ApiMail", "body_text": "Plain text body", "body_html": "

HTML body

" }' # Using inbox UUID (also works) curl -s -X POST https://api.apimail.cc/v1/messages/send \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from_inbox_id": "'$INBOX_ID'", "to": ["recipient@example.com"], "subject": "Hello from ApiMail", "body_text": "Plain text body" }' # With attachments (base64-encoded, 10MB total limit) curl -s -X POST https://api.apimail.cc/v1/messages/send \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "hello@example.com", "to": ["recipient@example.com"], "subject": "Invoice attached", "body_text": "Please find the invoice attached.", "attachments": [ { "filename": "invoice.pdf", "content_type": "application/pdf", "content_base64": "'$(base64 -i invoice.pdf)'" } ] }' ``` Attachments are supported on `send`, `reply`, `reply-all`, and `forward` endpoints. Dangerous file types (`.exe`, `.bat`, `.js`, etc.) are blocked. Response: ```json { "id": "send-job-uuid", "status": "sent", "message_id_header": "", "sent_at": "2026-03-05T...", "error_message": null } ``` #### Replying to a message ```bash curl -s -X POST https://api.apimail.cc/v1/messages/reply \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "hello@example.com", "in_reply_to_message_id": "'$MESSAGE_ID'", "to": ["recipient@example.com"], "body_text": "Thanks for your email!" }' ``` The server automatically sets `In-Reply-To`, `References`, and `Subject` (with `Re:` prefix) from the original message. The reply joins the same thread. #### Reply-all ```bash curl -s -X POST https://api.apimail.cc/v1/messages/reply-all \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "hello@example.com", "in_reply_to_message_id": "'$MESSAGE_ID'", "body_text": "Thanks everyone!" }' ``` Recipients are auto-derived: `to` is set to the original sender, `cc` includes all other original To/CC recipients (minus your own address). You can override either by providing `to` or `cc` explicitly. #### Forwarding a message ```bash curl -s -X POST https://api.apimail.cc/v1/messages/forward \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "hello@example.com", "message_id": "'$MESSAGE_ID'", "to": ["specialist@example.com"], "body_text": "FYI — see the original message below." }' ``` The original message body is included as quoted content. Subject gets a `Fwd:` prefix. No threading headers are set — the forward starts a new conversation. #### Before you can send Your domain must be `send_enabled`. This requires: 1. **Setup DKIM**: `POST /v1/domains/{id}/setup-dkim` — generates a keypair and returns the DNS TXT record to add 2. **Add DNS records**: SPF, DKIM, and DMARC TXT records at your registrar 3. **Verify**: `POST /v1/domains/{id}/verify` — checks all 5 DNS records and enables sending Required DNS records (returned by the API): - **SPF**: `v=spf1 ip4:199.127.109.122 -all` on your domain - **DKIM**: `v=DKIM1; k=rsa; p=...` on `om1._domainkey.yourdomain.com` - **DMARC**: `v=DMARC1; p=none; rua=mailto:dmarc@apimail.cc` on `_dmarc.yourdomain.com` #### Rate limits | Domain type | Daily limit | |-------------|-------------| | Shared domain (`@agentcourier.cc`) | 25 sends/day per inbox | | Custom domain (basic plan) | 200 sends/day | | Custom domain (pro plan) | 5,000 sends/day | Exceeding limits returns `429 Too Many Requests`. #### Idempotency Include an `idempotency_key` in send requests to prevent duplicate sends on retry. If the same key is used twice for the same account, the second request returns `409 Conflict`. ### Suppression List Recipients that hard-bounce are automatically added to the suppression list. Sending to a suppressed recipient returns `422`. | Method | Path | Scope | Description | |--------|------|-------|-------------| | GET | `/v1/suppression/` | read | List all suppressed email addresses. | | DELETE | `/v1/suppression/{email}` | admin | Remove an email from the suppression list. | ### Webhooks Webhooks let you receive real-time notifications when events occur (e.g. new email received). All webhook endpoints require `admin` scope. | Method | Path | Scope | Description | |--------|------|-------|-------------| | POST | `/v1/webhooks/` | admin | Create a webhook subscription. Returns a signing `secret`. | | GET | `/v1/webhooks/` | admin | List all webhooks. | | GET | `/v1/webhooks/{id}` | admin | Get a single webhook. | | PATCH | `/v1/webhooks/{id}` | admin | Update URL, events, or status (`active`/`paused`). | | DELETE | `/v1/webhooks/{id}` | admin | Delete a webhook. | | GET | `/v1/webhooks/{id}/deliveries` | admin | List recent delivery attempts for a webhook. | **Available events:** `message.received`, `message.sent`, `message.rejected` #### Setting up a webhook ```bash curl -s -X POST https://api.apimail.cc/v1/webhooks/ \ -H "X-API-Key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://yourapp.com/webhook", "events": ["message.received"]}' ``` Response: ```json { "id": "webhook-uuid", "url": "https://yourapp.com/webhook", "events": ["message.received"], "secret": "a1b2c3...64-char-hex-secret", "status": "active", "created_at": "2026-03-05T..." } ``` **Save the `secret`** — it's only returned on creation. You'll use it to verify webhook signatures. #### Webhook payload When an event fires, ApiMail sends a POST request to your URL: ```json { "event_id": "uuid", "event_type": "message.received", "created_at": "2026-03-05T10:30:00+00:00", "data": { "message_id": "uuid", "inbox_id": "uuid", "thread_id": "uuid", "from": "sender@example.com", "to": ["you@yourdomain.com"], "subject": "Hello" } } ``` Use the `message_id` to fetch the full message via `GET /v1/messages/{id}`. #### Verifying signatures Every webhook request includes an `X-ApiMail-Signature` header — an HMAC-SHA256 hex digest of the request body, signed with your secret. **Python example:** ```python import hmac, hashlib def verify_webhook(body: bytes, signature: str, secret: str) -> bool: expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) ``` **Node.js example:** ```javascript const crypto = require('crypto'); function verifyWebhook(body, signature, secret) { const expected = crypto.createHmac('sha256', secret).update(body).digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); } ``` #### Retry policy Failed deliveries are retried with exponential backoff: 1 min → 5 min → 30 min → 2 hr → 8 hr. After 5 failed attempts, the delivery is marked `dead`. Check delivery status via `GET /v1/webhooks/{id}/deliveries`. ### System | Method | Path | Scope | Description | |--------|------|-------|-------------| | GET | `/health` | none | Returns `{"status": "ok"}` | | GET | `/skill.md` | none | This file | ## Response Formats ### Message object ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "inbox_id": "uuid", "thread_id": "uuid", "direction": "inbound", "message_id_header": "", "in_reply_to": "", "subject": "Hello from ApiMail", "from_addr": "sender@example.com", "to_addrs": ["you@yourdomain.com"], "cc_addrs": [], "body_text": "Plain text body content", "body_html": "

HTML body content

", "status": "active", "received_at": "2025-01-15T10:30:00Z", "read_at": null, "size_bytes": 4521, "attachments": [ { "id": "uuid", "filename": "report.pdf", "content_type": "application/pdf", "size_bytes": 102400, "is_inline": false } ] } ``` ### Thread object ```json { "thread_id": "uuid", "inbox_id": "uuid", "subject": "Hello from ApiMail", "message_count": 3, "last_message_at": "2025-01-15T12:00:00Z", "from_addr": "sender@example.com" } ``` ### Paginated response All list endpoints return: ```json { "items": [...], "next_cursor": "base64-encoded-cursor-or-null", "has_more": true } ``` To paginate: pass the `next_cursor` value as `?cursor=` on the next request. When `has_more` is `false`, you've reached the end. ### Error response ```json { "detail": "Human-readable error message" } ``` HTTP status codes: `400` (bad request), `401` (invalid API key), `403` (insufficient scope), `404` (not found), `409` (conflict/duplicate), `422` (validation error, e.g. suppressed recipient), `429` (rate limit exceeded). ## Current Limitations - **No SDK** — use raw HTTP calls (curl, httpx, requests, fetch, etc.) ## Tips for AI Agents - **Get started instantly** — `POST /v1/inboxes/` with an empty body creates a receive-ready inbox in one call, no domain setup needed - Create a **dedicated inbox per agent or workflow** — use shared domain for quick experiments, custom domain for production - **Use email addresses instead of UUIDs** — all inbox endpoints accept `hello@yourdomain.com` as the identifier - **Use webhooks** to get notified instantly when email arrives — register a `message.received` webhook instead of polling - If you can't receive webhooks, **poll for new mail** with `GET /v1/inboxes/{email_or_id}/messages?unread=true` - Use **`?mark_read=true`** on `GET /v1/messages/{id}` to fetch and mark in one call — prevents reprocessing - **Thread IDs** group related messages — use `GET /v1/threads/{id}/messages` to get full conversation context - **`body_text`** is usually best for LLM consumption; `body_html` has richer formatting but needs parsing - **Reply to emails** with `POST /v1/messages/reply` — it auto-sets threading headers so the reply appears in the same conversation - **Use idempotency keys** on sends to safely retry without duplicate emails - Use **`GET https://api.apimail.cc/openapi.json`** to programmatically discover all endpoints and their schemas