# DuckMail API Reference # Base URL: https://api.duckmail.sbs # Authentication: Bearer Token or API Key (dk_xxx) # This file is designed to be sent to AI assistants for integration help. # All paths are relative to the Base URL. Send JSON request bodies with Content-Type: application/json. # IDs and credentials in examples are illustrative. Use values returned by the API; never send placeholders literally. # Response examples may omit additional fields or list items. A 204 response has no body: do not call response.json() on it. --- ## Authentication ### Bearer Token Obtain a token via POST /token with email address and password. Include in requests: Authorization: Bearer ### API Key Required for Microsoft hosted mailbox list/import/management. Optional for GET /domains and POST /accounts; required when using a private domain. Obtain from https://domain.duckmail.sbs Format: dk_ prefix. Include in requests: Authorization: Bearer dk_xxx --- ## SMTP and Shared Mailbox Endpoints ### [Domains] GET /domains Get available domain list. Returns private domains if API key is provided. Auth: Optional (API Key or Bearer Token) Query: page (default 1, 30 per page) Response: ```json { "hydra:member": [ { "id": "string", "domain": "example.com", "ownerId": null, "isVerified": true, "verificationToken": "duckmail-verify-xxx", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ], "hydra:totalItems": 10, "hydra:view": { "@id": "/domains?page=1", "@type": "PartialCollectionView", "hydra:first": "/domains?page=1", "hydra:last": "/domains?page=1" } } ``` Note: Only verified domains are returned. System domains have ownerId omitted or null and are visible to all. ### [Accounts] POST /accounts Create an SMTP temporary account. Use POST /accounts/imports for Microsoft hosting. API key required for private domains. Auth: Optional (API Key or Bearer Token) Request: ```json { "address": "user@duckmail.sbs", "password": "your_password", "expiresIn": 86400 } ``` Fields: - address (required): email address. Username (before @) >= 3 chars, domain must be verified. - password (required): >= 6 chars. - expiresIn (optional, integer, seconds): Account expiry. Omit = 24h auto-cleanup. Positive number = custom expiry in seconds. 0 or -1 = never expires. Note: only pass 0/-1 when you truly need a long-lived mailbox. For throwaway/verification-code usage, omit it or use a short expiry so the account is cleaned up automatically. Validation: address must contain @, username (before @) >= 3 chars, password >= 6 chars, domain must be verified. Response (201): ```json { "id": "string", "address": "user@duckmail.sbs", "authType": "email", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ``` ### [Auth] POST /token Get authentication token using email and password. Auth: None for password login. For hosted API-Key login, see Microsoft Hosted Mailboxes. Request: ```json { "address": "user@duckmail.sbs", "password": "your_password" } ``` Response: ```json { "id": "account-id", "token": "eyJhbGc..." } ``` ### [Accounts] GET /me Get current authenticated account info. Auth: Required (Bearer Token) Response: ```json { "id": "string", "address": "user@duckmail.sbs", "authType": "email", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ``` ### [Accounts] DELETE /accounts/{id} SMTP accounts: delete the currently logged-in account by ID. Hosted account removal requires an owner API Key; see Microsoft Hosted Mailboxes. Auth: Required (Bearer Token) Response: 204 No Content ### [Messages] GET /messages Get inbox message list (paginated, newest first). Auth: Required (Bearer Token) Query: page (default 1, 30 per page) Response: ```json { "hydra:member": [ { "id": "string", "msgid": "string", "accountId": "string", "from": { "name": "Sender", "address": "sender@example.com" }, "to": [{ "name": "You", "address": "you@duckmail.sbs" }], "subject": "Email Subject", "seen": false, "isDeleted": false, "hasAttachments": false, "size": 1024, "downloadUrl": "/serve/mailbox/...", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ], "hydra:totalItems": 5, "hydra:view": { "@id": "/messages", "@type": "PartialCollectionView", "hydra:first": "/messages?page=1", "hydra:last": "/messages?page=1" } } ``` Note: List view does not include text/html body content. ### [Messages] GET /messages/{id} Get full message details including body and attachments. Auth: Required (Bearer Token) Response: ```json { "id": "string", "msgid": "string", "accountId": "string", "from": { "name": "Sender", "address": "sender@example.com" }, "to": [{ "name": "You", "address": "you@duckmail.sbs" }], "subject": "Email Subject", "text": "Plain text body", "html": ["..."], "seen": false, "isDeleted": false, "hasAttachments": true, "size": 2048, "downloadUrl": "/serve/mailbox/...", "attachments": [ { "id": "0", "filename": "document.pdf", "contentType": "application/pdf", "disposition": "attachment", "transferEncoding": "", "related": false, "size": 1024, "downloadUrl": "/serve/mailbox/.../attach/0/document.pdf" } ], "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } ``` ### [Messages] PATCH /messages/{id} Mark a message as read. Auth: Required (Bearer Token) Response (200): ```json { "seen": true } ``` ### [Messages] DELETE /messages/{id} Delete a message by ID. Auth: Required (Bearer Token) Response: 204 No Content ### [Messages] GET /sources/{id} Get raw email source (RFC 822 format). Auth: Required (Bearer Token) Response: ```json { "id": "string", "downloadUrl": "/serve/mailbox/.../source", "data": "From: sender@example.com\nTo: ..." } ``` --- ## Error Response Format ```json { "error": "Error Type", "message": "Detailed error message" } ``` Status codes: - 400: Bad Request (invalid format) - 401: Unauthorized (missing or invalid token) - 403: Forbidden (no permission) - 404: Not Found - 410: Gone (resource removed or upstream sync state expired) - 409: Conflict (e.g. an idempotency key reused with different import content) - 422: Unprocessable Entity (validation failed or an SMTP email address already exists) - 424: Microsoft authorization needs renewal (UPSTREAM_REAUTH_REQUIRED) - 429: Rate limited; respect Retry-After in seconds - 502/503: Temporary upstream or service failure - 500: Internal Server Error --- ## Quick Start: SMTP Temporary Mailbox ```bash # 1. Create account (expiresIn: omit=24h, >0=custom seconds; 0/-1=never, only if you really need it) curl -X POST https://api.duckmail.sbs/accounts \ -H "Content-Type: application/json" \ -d '{"address": "test@duckmail.sbs", "password": "mypassword", "expiresIn": 86400}' # 2. Get token curl -X POST https://api.duckmail.sbs/token \ -H "Content-Type: application/json" \ -d '{"address": "test@duckmail.sbs", "password": "mypassword"}' # 3. Read messages curl https://api.duckmail.sbs/messages \ -H "Authorization: Bearer " # 4. Get message detail curl "https://api.duckmail.sbs/messages/" \ -H "Authorization: Bearer " # 5. Mark message as read curl -X PATCH "https://api.duckmail.sbs/messages/" \ -H "Authorization: Bearer " # 6. Get domains (with API key for private domains) curl https://api.duckmail.sbs/domains \ -H "Authorization: Bearer dk_your_api_key" ``` --- ## Microsoft Hosted Mailboxes ### Scope and credentials - Use POST /accounts/imports to add a Microsoft mailbox. POST /accounts is for SMTP temporary accounts only. - An owner API Key manages that owner's hosted accounts. A mailbox token reads one mailbox. Send exactly one credential in the Authorization header. - A DuckMail access password is separate from the Microsoft password. Microsoft clientId/refreshToken are only used to establish or replace hosting authorization. - Existing domain API Keys also manage hosted accounts. There is no per-key mailbox selection. Owner panel sessions can also access management endpoints; examples below use API Keys for public integrations. - Revoking an API Key invalidates mailbox tokens issued through that key. Independent password-login tokens are not revoked by that action. - No endpoint sends email or deletes the original Microsoft message. ### Recommended flow 1. New mailbox: POST /accounts/imports, then poll its statusUrl until status is completed. Inspect each row; completed does not mean every row succeeded. 2. Already imported: GET /accounts/hosted to find the account. Do not re-import it merely to sign in. 3. POST /token with either the owner API Key and address, or the address and DuckMail access password. 4. Use the returned mailbox token for GET /me, GET /messages and message detail/download endpoints. 5. Poll without overlapping requests, normally every 2 seconds. Honor Retry-After on 429; a 2-second poll interval is not a Microsoft synchronization guarantee. ### [Hosted Imports] POST /accounts/imports Create an asynchronous import job. Auth: Required (owner API Key) Headers: Content-Type: application/json; optional Idempotency-Key (up to 128 bytes). Request: ```json { "entries": [ { "address": "example@outlook.com", "password": "example-access-password", "protocol": "auto", "clientId": "11111111-2222-3333-4444-555555555555", "refreshToken": "", "label": "Test mailbox", "tags": ["testing"] } ] } ``` Fields: - entries (required): 1–500 entries per request; JSON body at most 5 MiB. - address (required): the Microsoft mailbox address; must match the authorized Microsoft identity. - password (required): independent DuckMail access password, at least 8 characters and at most 72 UTF-8 bytes. Import trims surrounding whitespace. - clientId (required): UUID of the original Microsoft application that issued the refresh token. Replace the example UUID with the real application ID. - refreshToken (required): that application's Microsoft refresh token, not a DuckMail mailbox token or Microsoft password. - protocol (optional): auto (default), graph or imap. - label (optional): up to 120 UTF-8 bytes. tags (optional): up to 10 strings, each up to 60 UTF-8 bytes. - source (optional): if supplied, must be microsoft. Omit expiresIn; hosted accounts do not use temporary-account expiry. - Compatibility: protocol/clientId/refreshToken may instead be nested in connection. Use either the flat form above or the nested form, not both. Protocol selection: - auto tries Graph first, then IMAP only when Graph explicitly lacks the required protocol permission. - Explicit graph or imap uses only that protocol. Throttling, network errors and invalid credentials do not cause automatic protocol switching. Response (202): ```json { "importId": "import-id", "status": "queued", "statusUrl": "/accounts/imports/import-id" } ``` Notes: - 202 means accepted for processing. Entry validation or Microsoft verification may still fail; inspect the job rows. - Reusing an Idempotency-Key with the same request returns the original import ID. Different content with the same key returns 409 IDEMPOTENCY_CONFLICT. - A mailbox already owned by the same user is skipped without overwriting its password or authorization. An address unavailable to this owner fails. - Current owner quota: 5,000 hosted accounts plus pending new-account imports. ### [Hosted Imports] GET /accounts/imports/{importId} Read job progress and per-entry results. Auth: Required (owner API Key) Path: importId from the import response, not an account ID. Response (200, example with one successful row): ```json { "id": "import-id", "status": "completed", "total": 1, "counts": { "succeeded": 1 }, "createdAt": "2026-09-16T10:00:00Z", "rows": [ { "row": 1, "address": "example@outlook.com", "status": "succeeded", "code": "", "message": "", "accountId": "account-id" } ] } ``` Status interpretation: - Job status is running while any row is queued/running; otherwise completed, including when rows failed or were cancelled. - Row status: queued, running, succeeded, skipped, failed or cancelled. row is a 1-based input index. - counts contains only statuses present in this job; treat missing counts as zero. - Use accountId from a succeeded row. A skipped existing account may also have accountId; a duplicate within the same batch can have an empty accountId. - For failed rows, inspect code/message. Credentials and passwords are never returned. ### [Hosted Imports] POST /accounts/imports/{importId}/retry Requeue eligible failed rows in the same job. Auth: Required (owner API Key) Request: No body required. Response (202): ```json { "importId": "import-id" } ``` Notes: Only failed rows with retained retry data in a job less than 24 hours old are retried. This does not correct invalid credentials or recreate completed rows. If retry data has expired, submit corrected entries as a new import. ### [Hosted Imports] POST /accounts/imports/{importId}/cancel Cancel unfinished rows and discard their pending credentials. Auth: Required (owner API Key) Request: No body required. Response: 204 No Content. Notes: Affects queued, running and failed rows; does not undo accounts already created. Poll the job to inspect final row results. ### [Hosted Accounts] GET /accounts/hosted List hosted accounts owned by the API Key's user. Auth: Required (owner API Key) Query: - page: default 1; 30 accounts per page. - q: optional address/label/tag search, up to 200 UTF-8 bytes. - status: optional filter, such as active, disabled, paused, ready, syncing, retrying or needs_reauth. Response (200): Hydra collection with hydra:member, hydra:totalItems and hydra:view, as in GET /domains. Members have source=microsoft, account status, hosting and capabilities. Notes: Account status (active/disabled) and hosting.status (connection/sync state) are separate. Management views expose management capabilities; mailbox-token views do not. ### [Hosted Auth] POST /token Obtain a 24-hour token for one hosted mailbox. Choose one login method. Method A — owner API Key, no password: Auth: Required (owner API Key) Request: ```json { "address": "example@outlook.com" } ``` Method B — independent DuckMail password: Auth: None Request: ```json { "address": "example@outlook.com", "password": "example-access-password" } ``` Response (200, both methods): ```json { "id": "account-id", "token": "" } ``` Notes: Reuse the mailbox token until it expires or is revoked; do not obtain a new token on every poll. Direct message-reading endpoints require this token, not the owner API Key. ### [Hosted Accounts] GET /me Read the mailbox identity and current hosting state. Auth: Required (mailbox token) Response (200, selected fields): ```json { "id": "account-id", "address": "example@outlook.com", "authType": "email", "source": "microsoft", "status": "active", "hosting": { "status": "ready", "protocol": "graph", "paused": false, "lastSuccessAt": "2026-09-16T10:05:00Z", "nextSyncAt": null, "initialSyncComplete": true, "stale": false, "truncated": false, "coverageFrom": "2026-09-09T10:00:00Z", "folders": ["inbox", "junk"] }, "capabilities": { "readMessages": true, "markSeenLocally": true, "hideMessagesLocally": true, "deleteAccount": false, "manageConnection": false } } ``` Notes: hosting may also include errorCode/errorMessage. lastSuccessAt and nextSyncAt can be null. A Microsoft authorization problem does not by itself invalidate DuckMail login or remove already synchronized records. ### [Hosted Messages] GET /messages Read locally synchronized message metadata; the response does not wait for a full Microsoft sync. Auth: Required (mailbox token) Query: - page: default 1; 30 messages per page, newest first. - folder: all (default), inbox or junk. - from, subject: optional case-insensitive substring filters, each up to 200 UTF-8 bytes. - receivedAfter: optional RFC3339 timestamp; cannot extend the normal recent-7-day listing window. - seen: optional true or false. Response (200): Hydra message collection as documented above, plus a top-level sync object with the same fields as GET /me's hosting object. Message bodies are not included. Interpretation: - hydra:totalItems counts matching local records, not the remote Microsoft mailbox total. - sync.initialSyncComplete=false: first synchronization is incomplete. - sync.stale=true: cached records may be out of date. Check sync.lastSuccessAt and sync.status. - sync.truncated=true: the indexed result is incomplete. - An empty list with incomplete/stale sync is not proof that the remote mailbox is empty. ### [Hosted Messages] GET /messages/{id} Read message details, body and attachment metadata. Auth: Required (mailbox token) Path: id from the message list, not the account ID or a Microsoft/IMAP identifier. Response (200): The shared message-detail structure above. Body/source may be fetched from Microsoft on demand and temporarily cached. Use returned downloadUrl values for source and attachment downloads. ### [Hosted Messages] GET /sources/{id} Read raw MIME in a JSON wrapper. Auth: Required (mailbox token) Response (200): ```json { "id": "message-id", "downloadUrl": "/messages/message-id/source", "data": "From: sender@example.com\r\nTo: example@outlook.com\r\n\r\nMessage body" } ``` ### [Hosted Downloads] GET /messages/{id}/source Download raw MIME directly. Auth: Required (mailbox token) Response: 200, Content-Type: message/rfc822, raw bytes; not JSON. Notes: Maximum source size is 25 MiB. Send Authorization when downloading; do not assume the URL is public. ### [Hosted Downloads] GET /messages/{id}/attachments/{attachmentId} Download an attachment. Auth: Required (mailbox token) Path: Use the returned attachment downloadUrl/ID from message details; do not invent attachment IDs. Response: 200, the attachment's Content-Type and raw bytes; not JSON. ### [Hosted Messages] PATCH /messages/{id} Mark a message as seen locally. Auth: Required (mailbox token) Request: No body required. This operation marks seen=true; it does not toggle unread or update Microsoft read state. Response (200): ```json { "seen": true } ``` ### [Hosted Messages] DELETE /messages/{id} Hide a message locally. Auth: Required (mailbox token) Response: 204 No Content. Notes: Does not delete the Microsoft original. Hidden messages no longer appear in the normal local list. ### [Hosted Accounts] POST /accounts/{id}/sync Schedule synchronization of one hosted mailbox. Auth: Required (owner API Key OR that mailbox's token) Request: No body required. Response (202): ```json { "status": "scheduled", "accountId": "account-id" } ``` Notes: 202 does not mean synchronization is complete. Inspect the sync field from GET /messages or the hosting field from GET /me afterward. A paused/disabled account returns 409; required Microsoft reauthorization returns 424. ### [Hosted Accounts] PATCH /accounts/{id} Update account settings. Auth: Required (owner API Key) Request (all fields optional; include only changes): ```json { "label": "Test mailbox", "tags": ["testing"], "paused": false, "status": "active" } ``` Fields: label up to 120 UTF-8 bytes; up to 10 tags of 60 UTF-8 bytes each; paused is boolean; status is active or disabled. Response: 204 No Content. Notes: Changing account status invalidates existing mailbox tokens. Pausing sync is separate from disabling the account. ### [Hosted Accounts] POST /accounts/{id}/password Reset the independent DuckMail access password. Auth: Required (owner API Key) Request: ```json { "password": "new-example-access-password" } ``` Validation: At least 8 characters and at most 72 UTF-8 bytes. Response: 204 No Content. Notes: Invalidates existing mailbox tokens, including API-Key-derived tokens. Obtain a new token afterward. Does not change the Microsoft password. ### [Hosted Connections] GET /accounts/{id}/connection Inspect the current connection. Auth: Required (owner API Key) Response (200): An object containing account (account view), hosting (hosting state) and clientId (original Microsoft application ID). No refresh token, access password or decrypted Microsoft credentials are returned. ### [Hosted Connections] POST /accounts/{id}/connection Replace Microsoft authorization asynchronously. Auth: Required (owner API Key) Headers: Content-Type: application/json; optional Idempotency-Key. Request: ```json { "protocol": "graph", "clientId": "11111111-2222-3333-4444-555555555555", "refreshToken": "" } ``` Response (202): ```json { "importId": "import-id" } ``` Notes: - Poll GET /accounts/imports/{importId} to check the result. Replacement requires an active account and the same Microsoft identity. - Keep the existing protocol. Omitting protocol or using auto still preserves the existing protocol; this endpoint cannot switch a Graph account to IMAP or vice versa. - Account ID and DuckMail access password remain unchanged. ### [Hosted Accounts] DELETE /accounts/{id} Remove hosting, credentials and related local cache. Auth: Required (owner API Key). A mailbox token cannot delete a hosted account. Response: 204 No Content. Notes: Does not delete the Microsoft mailbox or any original Microsoft messages. ### Microsoft-specific errors and limits - DuckMail 401: missing, invalid, expired or revoked DuckMail credentials. Do not treat it as a Microsoft refresh-token error. - 424 UPSTREAM_REAUTH_REQUIRED: the owner must replace Microsoft authorization. Repeated DuckMail logins do not fix it. - 429 UPSTREAM_THROTTLED: Microsoft requested backoff. Respect Retry-After; it is not proof of an IP abuse penalty. - 502/503: temporary upstream/service failure; use bounded retries and honor Retry-After when supplied. - Hosted errors can include code and retryable in addition to error/message. Branch on HTTP status and code, not localized message text. - Graph requires identity verification and mail-read permission. IMAP requires OAuth authorization and enabled IMAP access. Credentials that require an unsupported confidential-client secret cannot be imported. --- ## Rate Limits and Retry Behavior Current public deployment limits per IP: 1,000 requests/second with a valid JWT or API Key; 1,000 requests/second without authentication, except unauthenticated POST /accounts at 200 requests/second. Self-hosted limits depend on deployment settings. Repeated invalid requests can reduce the allowance to 1/10, then 1/100, then block the IP for 10 minutes. Duplicates, missing resources, configuration errors and authentication failures have more lenient thresholds than malformed requests or nonexistent creation domains. Limits do not replace account authentication or permissions. A rate-limit response is English JSON, HTTP 429, with Retry-After in seconds, for example: {"error":"Too Many Requests","message":"Too many abnormal requests from this IP. Access is temporarily rate limited. Please retry later.","detail":"Too many abnormal requests from this IP. Access is temporarily rate limited. Please retry later."} Wait at least Retry-After before making more requests to that backend. Do not repeatedly exchange tokens or recreate accounts on 429. If Retry-After is absent, use capped exponential backoff. After an IP block, allowance recovers from 1/100 one level at a time after each 5-minute violation-free period. A normal 2-second polling interval does not override cooldowns or guarantee Microsoft synchronization within 2 seconds. Check sync fields before interpreting empty hosted lists. No new mandatory parameters or client-side password hashing are needed. Microsoft UPSTREAM_THROTTLED also uses HTTP 429 and Retry-After; temporary upstream failures do not count toward IP abuse penalties. Handle DuckMail 401 separately from Microsoft UPSTREAM_REAUTH_REQUIRED (424). Self-hosters can exempt their Web server from backend rate limiting using a server-only shared secret. That is a deployment option, not a public client authentication method; API keys and mailbox tokens are still required where specified. A publicly accessible Web proxy with exemption enabled also exempts scripts using that proxy. SMTP message retention and account expiry are independent and depend on deployment settings; messages are not guaranteed to remain for three days. Microsoft hosting exposes recent synchronized metadata and reads/caches message content on demand. Local cache cleanup or hiding a hosted message never deletes Microsoft originals. The Web-only /api/mail and /api/sse proxy routes perform basic same-origin browser checks and return 403 for missing Fetch Metadata headers, cross-site requests or direct navigation. API integrations should call the Base URL above directly. These checks are not proof of a human browser and do not replace backend authentication.