# Generated by `php bin/console app:openapi:generate`; do not edit this file directly. openapi: 3.1.0 info: title: 'Junkmail API' version: 1.0.0 summary: 'Receive-only disposable-email automation' description: | Owner-scoped access to Junkmail aliases, received messages, and webhooks. Ordinary aliases and bearer-token automation are receive-only. Protected operator desks have owner-only browser-session compose and Sent endpoints. servers: - url: 'https://junksink.com/api/v1' tags: - name: Account - name: Aliases - name: Messages - name: Webhooks - name: Sharing - name: Exports - name: Testing x-junkmail-webhook-delivery: eventTypes: - message.received - message.otp_extracted - message.restored - alias.claimed - alias.paused - alias.burned - alias.expiring - webhook.test payloadSchema: '#/components/schemas/WebhookEvent' headers: - webhook-id - webhook-timestamp - webhook-signature signature: 'Standard Webhooks: sign webhook-id + "." + webhook-timestamp + "." + the exact raw body. webhook-signature contains space-separated v1, and v1a,. Either signature may be verified.' publicKeys: 'GET /.well-known/junkmail-webhooks.json on junksink.com returns the current and overlapping Ed25519 public keys with kid values.' replayProtection: 'Reject webhook-timestamp values older than five minutes and deduplicate webhook-id.' retries: 'Connection failures, timeouts, HTTP 429, and HTTP 5xx retry with jitter around 60, 300, 1800, 7200, 21600, 43200, and 86400 seconds (8 total attempts). Other non-2xx responses are terminal. An endpoint with only failures for three consecutive days is disabled and its owner notified.' idempotency: 'The evt_-prefixed ULID is stable across every retry and manual redelivery. Consumers should deduplicate by webhook-id.' sharedMailboxes: "A webhook registered by a guest on a mailbox shared with them receives the same signed envelope with shared_mailbox true and owner_handle set to the owner's pseudonymous handle. Real addresses and the owner's alias label never cross the account boundary. The alias owner can revoke any guest endpoint on their mailbox at any time without removing the guest; delivery then stops immediately and queued retries are abandoned." workedExample: '#/components/examples/WebhookMessageReceived' security: - bearerAuth: [] - sessionCookie: [] paths: '/desk-mail/{aliasId}': parameters: - name: aliasId in: path required: true schema: type: integer minimum: 1 get: tags: - Messages summary: 'Retained outgoing mail for an owned protected desk' parameters: - name: page in: query schema: type: integer minimum: 1 default: 1 responses: '200': description: 'Up to fifty outgoing records, newest first, retained for ninety days' content: application/json: schema: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/DeskSentMail' '404': description: 'No active protected desk owned by this account' security: - sessionCookie: [] x-junkmail-session-only: true post: tags: - Messages summary: 'Send plaintext from an owned protected desk' description: 'Browser session and X-CSRF-Token required. No bearer tokens, attachments or additional fields. Ten attempts per account per hour and sixty total.' requestBody: required: true content: application/json: schema: type: object additionalProperties: false required: - to - subject - body properties: to: type: string format: email subject: type: string description: 'UTF-8, up to 998 bytes; no CR or LF' body: type: string description: 'Plaintext UTF-8, up to 65536 bytes' message_id: type: - integer - 'null' minimum: 1 responses: '201': description: 'Sent record identifier' content: application/json: schema: type: object required: - id properties: id: type: integer minimum: 1 '403': description: 'Invalid CSRF token or bearer authentication' '404': description: 'No active protected desk owned by this account' '422': description: 'Invalid content, recipient or reply target' '429': description: 'Account or global send limit reached' '503': description: 'Sending unavailable; check Sent before retrying' security: - sessionCookie: [] x-junkmail-session-only: true '/desk-mail/{aliasId}/reply/{messageId}': parameters: - name: aliasId in: path required: true schema: type: integer minimum: 1 - name: messageId in: path required: true schema: type: integer minimum: 1 get: tags: - Messages summary: 'Prefill a reply to retained mail in an owned protected desk' responses: '200': description: 'Correspondent, reply subject and original message identifier' content: application/json: schema: type: object required: - to - subject - message_id properties: to: type: string format: email subject: type: string message_id: type: integer minimum: 1 '404': description: 'No active protected desk owned by this account' '422': description: 'Unavailable or mismatched reply target' security: - sessionCookie: [] x-junkmail-session-only: true /bootstrap: get: tags: - Account summary: 'Bootstrap API-token account context' description: 'Bearer responses omit browser-only CSRF, Mercure-cookie, and push settings.' responses: '200': description: 'Account, aliases, domains, and tags' content: application/json: schema: $ref: '#/components/schemas/BootstrapResponse' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read /me: get: tags: - Account summary: 'Fetch the authenticated account' responses: '200': description: 'Account and resolved plan limits' content: application/json: schema: $ref: '#/components/schemas/Account' '401': $ref: '#/components/responses/InvalidToken' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read /storage: get: tags: - Account summary: 'Fetch mailbox storage usage' responses: '200': description: 'Used, quota, remaining bytes, and usage ratio' content: application/json: schema: $ref: '#/components/schemas/StorageUsage' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read /tags: get: tags: - Messages summary: 'List tags attached to owned messages' responses: '200': description: 'Owned tags with their message counts' content: application/json: schema: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/TagWithCount' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read post: tags: - Messages summary: 'Create an owned tag' description: 'Browser-session only; requires X-CSRF-Token.' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object required: - name properties: name: type: string minLength: 1 maxLength: 64 color: type: - string - 'null' pattern: '^#[0-9A-Fa-f]{6}$' responses: '201': description: 'Created tag' content: application/json: schema: $ref: '#/components/schemas/Tag' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '409': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' security: - sessionCookie: [] x-junkmail-session-only: true '/tags/{id}': parameters: - name: id in: path required: true schema: type: integer minimum: 1 patch: tags: - Messages summary: 'Update an owned tag' description: 'Browser-session only; requires X-CSRF-Token.' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object properties: name: type: string minLength: 1 maxLength: 64 color: type: - string - 'null' pattern: '^#[0-9A-Fa-f]{6}$' responses: '200': description: 'Updated tag' content: application/json: schema: $ref: '#/components/schemas/Tag' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '404': $ref: '#/components/responses/Problem' '409': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' security: - sessionCookie: [] x-junkmail-session-only: true delete: tags: - Messages summary: 'Delete an owned tag' description: 'Browser-session only; requires X-CSRF-Token.' parameters: - $ref: '#/components/parameters/CsrfToken' responses: '204': description: Deleted '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '404': $ref: '#/components/responses/Problem' security: - sessionCookie: [] x-junkmail-session-only: true /domains: get: tags: - Aliases summary: 'List receiving domains available for alias generation' responses: '200': description: 'Receiving domains' content: application/json: schema: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/Domain' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read /aliases: get: tags: - Aliases summary: 'List owned aliases' parameters: - name: includeBurned in: query schema: type: boolean default: false responses: '200': description: 'Owned aliases' content: application/json: schema: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/Alias' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read post: tags: - Aliases summary: 'Claim a reserved alias candidate' description: 'Cookie-session calls also require X-CSRF-Token. Bearer calls are stateless.' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object required: - aliasId - reservationToken properties: aliasId: type: integer minimum: 1 reservationToken: type: string label: type: - string - 'null' note: type: - string - 'null' selfDestructAt: type: - string - 'null' format: date-time description: 'Optional Premium/Pro self-destruct deadline. The alias is burned automatically at this time.' examples: worked: $ref: '#/components/examples/AliasClaimRequest' responses: '201': description: 'Claimed alias' content: application/json: schema: $ref: '#/components/schemas/Alias' examples: worked: $ref: '#/components/examples/AliasClaimResponse' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '409': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: manage-aliases /aliases/candidates: post: tags: - Aliases summary: 'Reserve a new candidate set' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object required: - domainId - style properties: domainId: type: integer minimum: 1 style: type: string enum: - words - pronounceable - numeric responses: '200': description: 'Short-lived reservation containing five candidates' content: application/json: schema: $ref: '#/components/schemas/CandidateReservation' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: manage-aliases delete: tags: - Aliases summary: 'Release the current candidate reservation' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: false content: application/json: schema: type: object properties: reservationToken: type: string responses: '200': description: 'Number of released candidates' content: application/json: schema: $ref: '#/components/schemas/CandidateRelease' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: manage-aliases /aliases/candidates/release: post: tags: - Aliases summary: 'Release the current candidate reservation' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: false content: application/json: schema: type: object properties: reservationToken: type: string responses: '200': description: 'Number of released candidates' content: application/json: schema: $ref: '#/components/schemas/CandidateRelease' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: manage-aliases '/aliases/{id}': parameters: - $ref: '#/components/parameters/AliasId' patch: tags: - Aliases summary: 'Update alias metadata or lifecycle state' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object properties: label: type: - string - 'null' note: type: - string - 'null' action: type: string enum: - pause - resume - burn status: type: string enum: - active - paused - burned responses: '200': description: 'Updated owned alias' content: application/json: schema: $ref: '#/components/schemas/Alias' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: manage-aliases delete: tags: - Aliases summary: 'Irreversibly burn an owned alias' parameters: - $ref: '#/components/parameters/CsrfToken' responses: '200': description: 'Burned alias' content: application/json: schema: $ref: '#/components/schemas/Alias' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: manage-aliases '/aliases/{id}/pause': post: tags: - Aliases summary: 'Pause inbound delivery for an owned alias' parameters: - $ref: '#/components/parameters/AliasId' - $ref: '#/components/parameters/CsrfToken' responses: '200': description: 'Paused alias' content: application/json: schema: $ref: '#/components/schemas/Alias' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: manage-aliases '/aliases/{id}/timer': parameters: - $ref: '#/components/parameters/AliasId' put: tags: - Aliases summary: 'Set or extend an alias self-destruct timer' description: 'Premium/Pro entitlement. Setting a new future deadline replaces the prior deadline and resets the 24-hour notice marker.' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object required: - selfDestructAt additionalProperties: false properties: selfDestructAt: type: string format: date-time responses: '200': description: 'Updated timer state' content: application/json: schema: $ref: '#/components/schemas/AliasTimer' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '404': $ref: '#/components/responses/Problem' '409': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: manage-aliases delete: tags: - Aliases summary: 'Cancel an alias self-destruct timer' description: 'Cancellation remains available after downgrade, including while a preserved timer is suspended.' parameters: - $ref: '#/components/parameters/CsrfToken' responses: '200': description: 'Cleared timer state' content: application/json: schema: $ref: '#/components/schemas/AliasTimer' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '409': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: manage-aliases '/aliases/{id}/resume': post: tags: - Aliases summary: 'Resume inbound delivery for an owned alias' parameters: - $ref: '#/components/parameters/AliasId' - $ref: '#/components/parameters/CsrfToken' responses: '200': description: 'Active alias' content: application/json: schema: $ref: '#/components/schemas/Alias' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: manage-aliases '/aliases/{id}/burn': post: tags: - Aliases summary: 'Irreversibly burn an owned alias' parameters: - $ref: '#/components/parameters/AliasId' - $ref: '#/components/parameters/CsrfToken' responses: '200': description: 'Burned alias' content: application/json: schema: $ref: '#/components/schemas/Alias' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: manage-aliases '/aliases/{id}/share': parameters: - $ref: '#/components/parameters/AliasId' - $ref: '#/components/parameters/CsrfToken' delete: tags: - Sharing summary: 'Leave a mailbox somebody shared with you' description: 'Ends your own access to a mailbox another account owns. Access stops immediately and nothing you did in that mailbox is kept: your read, starred, archived and pinned state on its mail, and your own tags on it, are deleted. An account with no live share on the alias is answered exactly as a stranger is.' responses: '204': description: 'Access ended' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: manage-aliases '/aliases/{id}/share-grants': parameters: - $ref: '#/components/parameters/AliasId' get: tags: - Sharing summary: 'List guest webhook and API token grants on an owned alias' description: 'Owner-only oversight of a shared mailbox. Guests hold full API and webhook access, so this lists every guest endpoint and every guest API token that reaches this alias. Endpoints are identified by host only, because a webhook path can carry a secret, and neither endpoint names nor token names cross the account boundary.' responses: '200': description: 'Guest grants on the alias' content: application/json: schema: type: object required: - items additionalProperties: false properties: items: type: array items: $ref: '#/components/schemas/ShareGrant' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read '/aliases/{id}/share-grants/acknowledge': post: tags: - Sharing summary: 'Mark guest grants on an owned alias as seen' description: 'Clears the isNewToOwner flag raised the first time a guest registers a webhook against this alias.' parameters: - $ref: '#/components/parameters/AliasId' - $ref: '#/components/parameters/CsrfToken' responses: '204': description: Acknowledged '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: manage-aliases '/aliases/{id}/share-grants/{kind}/{subjectId}': delete: tags: - Sharing summary: 'Revoke one guest webhook or API token on an owned alias' description: 'Ends one grant without removing the guest, who keeps the mailbox. A revoked webhook stops receiving events immediately and any delivery still queued or retrying for this mailbox is abandoned; a revoked API token stops resolving this mailbox on its next request. Revocation is permanent for that endpoint or token: re-registering it does not restore access.' parameters: - $ref: '#/components/parameters/AliasId' - name: kind in: path required: true schema: type: string enum: - webhooks - api-tokens - name: subjectId in: path required: true description: 'The guest webhook identifier or the guest API token identifier, as listed by GET /aliases/{id}/share-grants.' schema: type: integer minimum: 1 - $ref: '#/components/parameters/CsrfToken' responses: '204': description: Revoked '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: manage-aliases /share-grants/revoked: get: tags: - Sharing summary: 'List your access to shared mailboxes that has stopped' description: 'Explains, to the guest, why their webhooks, tokens or reads stopped reaching a shared mailbox: an owner refusing one grant, the share being suspended, or the share ending. Poll this before assuming an outage. Reversible and permanent stops are distinguished by the reversible flag, so a client can retry rather than tear an integration down. What ended a share is deliberately not reported - removal, a burned alias, a self-destruct timer, an operator and an erased account are one answer - and a suspended or ended share reports the alias identifier without its address, because access ends and nothing is retained.' responses: '200': description: 'Revoked grants held by the calling account' content: application/json: schema: type: object required: - items additionalProperties: false properties: items: type: array items: $ref: '#/components/schemas/RevokedShareGrant' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read /messages: get: tags: - Messages summary: 'List or search owned messages' parameters: - name: q in: query description: 'Full-text query over the mailbox. Combined with trashed=true it is answered from the database instead, and then matches the subject and the sender only - see that parameter.' schema: type: string - name: alias in: query schema: type: integer minimum: 1 - name: domain in: query schema: type: string - name: unread in: query schema: type: boolean - name: hasAttachment in: query schema: type: boolean - name: starred in: query schema: type: boolean - name: pinned in: query schema: type: boolean - name: tag in: query schema: type: integer minimum: 1 - name: archived in: query schema: type: boolean default: false - name: trashed in: query description: 'Return the trash instead of the mailbox. The two sets never overlap, and the archived filter does not apply to the trash. Trashed mail leaves the search index - its document is removed exactly as a permanent delete removes it - so once those removals are projected, however much is in the trash does not affect a mailbox search; a backlog of more than 10000 matching just-deleted messages can briefly hide an older mailbox result until the projection catches up. A search of the trash itself matches the subject and the sender rather than the full text. A restored message is findable in the mailbox by text again once its projection runs, the same short delay a newly arrived message has.' schema: type: boolean default: false - name: excludeShared in: query description: 'Leave out mail that arrived in a mailbox another account shared with you. The page, the cursor and the total all describe the same narrowed set.' schema: type: boolean default: false - name: dateFrom in: query schema: type: string format: date-time - name: dateTo in: query schema: type: string format: date-time - name: cursor in: query schema: type: string - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 50 - name: sort in: query schema: type: string enum: - newest - oldest default: newest responses: '200': description: 'Cursor page of messages' content: application/json: schema: $ref: '#/components/schemas/MessagePage' examples: worked: $ref: '#/components/examples/MessagePageResponse' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '429': $ref: '#/components/responses/RateLimited' '503': $ref: '#/components/responses/Problem' x-junkmail-token-scope: read /messages/bulk: post: tags: - Messages summary: 'Apply one mailbox action to up to 100 owned messages' description: 'Browser-session only; requires X-CSRF-Token. `delete` moves the messages to the trash on a plan that has one and destroys them on a plan that does not; `delete_forever` always destroys them; `restore` takes them back out of the trash. Read `outcomes` rather than the counts to learn which identifier went where.' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object required: - ids - action properties: ids: type: array minItems: 1 maxItems: 100 uniqueItems: true items: type: integer minimum: 1 action: type: string enum: - mark_read - mark_unread - star - unstar - pin - unpin - archive - unarchive - add_tag - remove_tag - delete - restore - delete_forever tagId: type: integer minimum: 1 responses: '200': description: 'Counts of changed and deleted messages' content: application/json: schema: $ref: '#/components/schemas/BulkMessageResult' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '404': $ref: '#/components/responses/Problem' '409': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' security: - sessionCookie: [] x-junkmail-session-only: true /messages/trash/empty: post: tags: - Messages summary: 'Destroy everything in the trash, of one mailbox or of all of them' description: 'Bounded per request: repeat while `remaining` is above zero. Requires X-CSRF-Token from a browser session.' parameters: - $ref: '#/components/parameters/CsrfToken' - name: alias in: query description: 'Empty only this mailbox trash. Omitted, every mailbox you may delete from is emptied.' schema: type: integer minimum: 1 responses: '200': description: 'Messages destroyed and messages still in the trash' content: application/json: schema: $ref: '#/components/schemas/EmptyTrashResult' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: delete '/messages/{id}': parameters: - $ref: '#/components/parameters/MessageId' get: tags: - Messages summary: 'Fetch owned message metadata and parsed body' responses: '200': description: 'Message detail including parsed text, headers, body URLs, and attachments' content: application/json: schema: $ref: '#/components/schemas/MessageDetail' examples: worked: $ref: '#/components/examples/MessageDetailResponse' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read patch: tags: - Messages summary: 'Mark an owned message read or unread' description: 'Bearer tokens may update only isRead. Browser sessions may also update starred, pinned, archived, and tagIds and require X-CSRF-Token.' x-junkmail-bearer-fields: - isRead parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object additionalProperties: false properties: isRead: type: boolean starred: type: boolean pinned: type: boolean archived: type: boolean tagIds: type: array maxItems: 50 uniqueItems: true items: type: integer minimum: 1 responses: '200': description: 'Updated message flags and tags' content: application/json: schema: $ref: '#/components/schemas/MessageState' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '409': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read delete: tags: - Messages summary: 'Delete an owned message, to the trash or permanently' description: "On a plan with a trash the message moves there and can be restored until its purge date; on a plan without one it and its stored content are destroyed. 204 either way. Which one you are about to get is the mailbox *owner*'s plan, not yours: read `trash.enabled` on the alias before deleting, and compute the purge deadline as the earlier of the message's expiresAt and now plus `trash.retentionDays`. Repeating this on a message already in the trash is idempotent and does not destroy it - use permanent=1 for that." parameters: - $ref: '#/components/parameters/CsrfToken' - name: permanent in: query description: 'Skip the trash and destroy the message and its stored content, whether or not it is currently in the trash.' schema: type: boolean default: false responses: '204': description: Deleted '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: delete '/messages/{id}/restore': parameters: - $ref: '#/components/parameters/MessageId' post: tags: - Messages summary: 'Take an owned message back out of the trash' description: 'Idempotent: a message that is not in the trash answers 204 unchanged. Pins, tags and every reader private state survive the round trip because trashing never cleared them.' parameters: - $ref: '#/components/parameters/CsrfToken' responses: '204': description: Restored '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: delete '/messages/{id}/html': get: tags: - Messages summary: 'Fetch the sanitized HTML body' description: 'Returned with a restrictive sandbox CSP. Remote images are blocked by default.' parameters: - $ref: '#/components/parameters/MessageId' - name: remote in: query schema: type: integer enum: - 0 - 1 responses: '200': description: 'Sanitized HTML' content: text/html: schema: type: string '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read '/messages/{id}/raw': get: tags: - Messages summary: 'Download the original owned RFC 5322 message' parameters: - $ref: '#/components/parameters/MessageId' responses: '200': description: 'Raw message' content: message/rfc822: schema: type: string format: binary '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read '/messages/{id}/remote-images/preference': put: tags: - Messages summary: 'Set the sender-scoped remote-image preference' description: 'Browser-session only; requires X-CSRF-Token.' parameters: - $ref: '#/components/parameters/MessageId' - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object required: - alwaysLoad additionalProperties: false properties: alwaysLoad: type: boolean responses: '200': description: 'Updated preference' content: application/json: schema: $ref: '#/components/schemas/RemoteImagePreference' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '404': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' security: - sessionCookie: [] x-junkmail-session-only: true '/messages/{messageId}/attachments/{attachmentId}': get: tags: - Messages summary: 'Download an attachment belonging to an owned message' description: 'Append confirm=1 when metadata marks the attachment dangerous.' parameters: - name: messageId in: path required: true schema: type: integer minimum: 1 - name: attachmentId in: path required: true schema: type: integer minimum: 1 - name: confirm in: query schema: type: integer enum: - 1 responses: '200': description: 'Attachment bytes using the stored MIME type; dangerous files are served as application/octet-stream' content: '*/*': schema: type: string format: binary '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '409': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read '/messages/{messageId}/attachments/{attachmentId}/preview': get: tags: - Messages summary: 'Preview a safe, supported attachment from an owned message' description: 'Returned with a restrictive sandbox CSP; dangerous or unsupported types return 404.' parameters: - name: messageId in: path required: true schema: type: integer minimum: 1 - name: attachmentId in: path required: true schema: type: integer minimum: 1 responses: '200': description: 'Preview bytes' content: application/octet-stream: schema: type: string format: binary '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read /push/settings: get: tags: - Account summary: 'Fetch browser push settings and subscriptions' description: 'Browser-session only.' responses: '200': description: 'Push settings' content: application/json: schema: $ref: '#/components/schemas/PushSettings' '401': $ref: '#/components/responses/InvalidToken' security: - sessionCookie: [] x-junkmail-session-only: true put: tags: - Account summary: 'Enable or disable browser push globally' description: 'Browser-session only; requires X-CSRF-Token.' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object required: - enabled additionalProperties: false properties: enabled: type: boolean responses: '200': description: 'Updated push settings' content: application/json: schema: $ref: '#/components/schemas/PushSettings' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' security: - sessionCookie: [] x-junkmail-session-only: true /mailbox/density: put: tags: - Account summary: 'Set the message-list density for the account' description: 'Browser-session only; requires X-CSRF-Token. Stored on the account so every device agrees.' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object required: - density additionalProperties: false properties: density: $ref: '#/components/schemas/MailboxDensity' responses: '200': description: 'Updated display preference' content: application/json: schema: type: object required: - density additionalProperties: false properties: density: $ref: '#/components/schemas/MailboxDensity' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '404': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' security: - sessionCookie: [] x-junkmail-session-only: true /push/nudge: delete: tags: - Account summary: 'Dismiss the one-time browser-notification offer' description: 'Browser-session only; requires X-CSRF-Token. Remembered for the account rather than for the browser.' parameters: - $ref: '#/components/parameters/CsrfToken' responses: '200': description: 'Updated push settings' content: application/json: schema: $ref: '#/components/schemas/PushSettings' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '404': $ref: '#/components/responses/Problem' security: - sessionCookie: [] x-junkmail-session-only: true /push/subscriptions: post: tags: - Account summary: 'Register or refresh a browser PushSubscription' description: 'Browser-session only; requires X-CSRF-Token. The endpoint must be public HTTPS and passes SSRF checks.' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object required: - endpoint - keys properties: endpoint: type: string format: uri pattern: '^https://' contentEncoding: type: string enum: - aes128gcm - aesgcm default: aes128gcm keys: type: object required: - p256dh - auth properties: p256dh: type: string auth: type: string responses: '201': description: 'Registered subscription' content: application/json: schema: $ref: '#/components/schemas/PushSubscription' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '409': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' security: - sessionCookie: [] x-junkmail-session-only: true '/push/subscriptions/{id}': delete: tags: - Account summary: 'Remove an owned browser push subscription' description: 'Browser-session only; requires X-CSRF-Token.' parameters: - name: id in: path required: true schema: type: integer minimum: 1 - $ref: '#/components/parameters/CsrfToken' responses: '204': description: Removed '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '404': $ref: '#/components/responses/Problem' security: - sessionCookie: [] x-junkmail-session-only: true '/push/subscriptions/{id}/test': post: tags: - Account summary: 'Queue a harmless test notification for an owned subscription' description: 'Browser-session only; requires X-CSRF-Token. The accepted request performs no outbound delivery; an authenticated Messenger job stages the test for scheduler delivery.' parameters: - name: id in: path required: true schema: type: integer minimum: 1 - $ref: '#/components/parameters/CsrfToken' responses: '202': description: 'Test delivery accepted for asynchronous processing' content: application/json: schema: type: object required: - queued - sent additionalProperties: false properties: queued: type: boolean const: true sent: type: boolean const: true deprecated: true description: 'Compatibility acceptance flag; true means queued, not delivered.' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/PushTestRateLimited' '503': $ref: '#/components/responses/Problem' security: - sessionCookie: [] x-junkmail-session-only: true '/push/aliases/{id}': put: tags: - Aliases summary: 'Enable or disable push for an owned alias' description: 'Browser-session only; requires X-CSRF-Token.' parameters: - name: id in: path required: true schema: type: integer minimum: 1 - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object required: - enabled additionalProperties: false properties: enabled: type: boolean responses: '200': description: 'Updated alias' content: application/json: schema: $ref: '#/components/schemas/Alias' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '404': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' security: - sessionCookie: [] x-junkmail-session-only: true /exports: get: tags: - Exports summary: 'List retained mailbox exports' description: 'Pro only. Downgrade preserves archives but suspends this API until Pro resumes.' responses: '200': description: 'Mailbox exports' content: application/json: schema: type: object required: - items properties: items: type: array items: $ref: '#/components/schemas/MailboxExport' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read post: tags: - Exports summary: 'Queue a whole-mailbox or per-alias EML export' description: 'Pro only. Cookie-session calls require X-CSRF-Token; bearer calls are stateless.' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: false content: application/json: schema: type: object additionalProperties: false properties: aliasId: type: - integer - 'null' minimum: 1 examples: worked: $ref: '#/components/examples/MailboxExportRequest' responses: '202': description: 'Export queued' headers: Location: schema: type: string content: application/json: schema: $ref: '#/components/schemas/MailboxExport' examples: worked: $ref: '#/components/examples/MailboxExportResponse' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '404': $ref: '#/components/responses/Problem' '409': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read '/exports/{id}': get: tags: - Exports summary: 'Fetch an owned mailbox export status' parameters: - name: id in: path required: true schema: type: integer minimum: 1 responses: '200': description: 'Export status' content: application/json: schema: $ref: '#/components/schemas/MailboxExport' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read '/exports/{id}/download': get: tags: - Exports summary: 'Download a ready owned mailbox export' parameters: - name: id in: path required: true schema: type: integer minimum: 1 responses: '200': description: 'Private ZIP of EML files and manifest' content: application/zip: schema: type: string format: binary '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: read /test/messages: post: tags: - Testing summary: 'Inject deterministic mail through the normal receive pipeline' description: 'Pro only. Never opens SMTP or sends mail; test messages count toward storage and expire after seven days.' parameters: - $ref: '#/components/parameters/CsrfToken' - name: aliasId in: query description: 'Required only for message/rfc822 bodies.' schema: type: integer minimum: 1 requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TestMessageInput' examples: worked: $ref: '#/components/examples/TestMessageRequest' message/rfc822: schema: type: string format: binary maxLength: 15728640 responses: '202': description: 'Durable parser stub accepted' headers: Location: schema: type: string content: application/json: schema: $ref: '#/components/schemas/InjectedTestMessage' examples: worked: $ref: '#/components/examples/TestMessageResponse' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/Problem' '404': $ref: '#/components/responses/Problem' '415': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: test /webhooks: get: tags: - Webhooks summary: 'List owned webhook endpoints' responses: '200': description: 'Webhooks and plan limit' content: application/json: schema: $ref: '#/components/schemas/WebhookList' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: webhooks post: tags: - Webhooks summary: 'Create a webhook; its signing secret is shown once' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object required: - name - url - subscription properties: name: type: string maxLength: 80 url: type: string format: uri pattern: '^https://' description: 'Public HTTPS endpoint on port 443; credentials, fragments, private/reserved IPs, and unsafe DNS resolutions are rejected.' subscription: $ref: '#/components/schemas/WebhookSubscription' spki_pin: type: - string - 'null' pattern: '^sha256/[A-Za-z0-9+/]{43}=$' description: 'Optional SHA-256 SPKI pin for the receiver TLS public key.' include_text_body: type: boolean default: false description: 'Add sanitized plaintext truncated to 32 KiB; HTML and attachments are never inlined.' include_message_restored: type: boolean default: false description: 'Also deliver message.restored when a message is recovered from the trash. Off by default so an existing integration keeps receiving exactly the event types it already handles.' examples: worked: $ref: '#/components/examples/WebhookCreateRequest' responses: '201': description: 'Created webhook and one-time signing secret' content: application/json: schema: $ref: '#/components/schemas/WebhookWithSecret' examples: worked: $ref: '#/components/examples/WebhookCreateResponse' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '409': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: webhooks '/webhooks/{id}': parameters: - name: id in: path required: true schema: type: integer minimum: 1 patch: tags: - Webhooks summary: 'Update or pause an owned webhook' parameters: - $ref: '#/components/parameters/CsrfToken' requestBody: required: true content: application/json: schema: type: object properties: name: type: string minLength: 1 maxLength: 80 url: type: string format: uri pattern: '^https://' subscription: $ref: '#/components/schemas/WebhookSubscription' spki_pin: type: - string - 'null' pattern: '^sha256/[A-Za-z0-9+/]{43}=$' include_text_body: type: boolean include_message_restored: type: boolean status: type: string enum: - active - paused responses: '200': description: 'Updated webhook' content: application/json: schema: $ref: '#/components/schemas/Webhook' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '422': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: webhooks delete: tags: - Webhooks summary: 'Delete an owned webhook' parameters: - $ref: '#/components/parameters/CsrfToken' responses: '204': description: Deleted '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: webhooks '/webhooks/{id}/rotate-secret': post: tags: - Webhooks summary: 'Rotate and show a webhook signing secret once' parameters: - name: id in: path required: true schema: type: integer minimum: 1 - $ref: '#/components/parameters/CsrfToken' responses: '200': description: 'Webhook and one-time signing secret' content: application/json: schema: $ref: '#/components/schemas/WebhookWithSecret' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: webhooks '/webhooks/{id}/deliveries': get: tags: - Webhooks summary: "List an owned webhook's delivery attempts" parameters: - name: id in: path required: true schema: type: integer minimum: 1 - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 50 responses: '200': description: 'Delivery attempts' content: application/json: schema: $ref: '#/components/schemas/WebhookDeliveryPage' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: webhooks '/webhooks/{id}/test': post: tags: - Webhooks summary: 'Queue a harmless webhook.test event' parameters: - name: id in: path required: true schema: type: integer minimum: 1 - $ref: '#/components/parameters/CsrfToken' responses: '202': description: 'Queued webhook.test delivery' content: application/json: schema: $ref: '#/components/schemas/WebhookDeliveryQueued' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '409': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: webhooks '/webhooks/deliveries/{id}/redeliver': post: tags: - Webhooks summary: 'Redeliver a retained owned event with the same webhook-id' description: 'Resets the retry cycle while preserving prior attempt history. Payloads and logs expire after 30 days.' parameters: - name: id in: path required: true schema: type: integer minimum: 1 - $ref: '#/components/parameters/CsrfToken' responses: '202': description: 'Redelivery queued' content: application/json: schema: $ref: '#/components/schemas/WebhookDeliveryQueued' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '404': $ref: '#/components/responses/Problem' '409': $ref: '#/components/responses/Problem' '410': $ref: '#/components/responses/Problem' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: webhooks /webhooks/deliveries: get: tags: - Webhooks summary: 'List recent delivery attempts across owned webhooks' parameters: - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 50 responses: '200': description: 'Delivery attempts' content: application/json: schema: $ref: '#/components/schemas/WebhookDeliveryPage' '401': $ref: '#/components/responses/InvalidToken' '403': $ref: '#/components/responses/InsufficientScope' '429': $ref: '#/components/responses/RateLimited' x-junkmail-token-scope: webhooks components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: jm_pat description: 'Scoped personal access token. Do not also send a browser session cookie.' sessionCookie: type: apiKey in: cookie name: junkmail_session description: 'Browser mailbox session; state changes also require X-CSRF-Token.' parameters: CsrfToken: name: X-CSRF-Token in: header required: false description: 'Required for cookie-session state changes; omit for bearer authentication.' schema: type: string AliasId: name: id in: path required: true schema: type: integer minimum: 1 MessageId: name: id in: path required: true schema: type: integer minimum: 1 responses: InvalidToken: description: 'A bearer request returns error code invalid_token for malformed, expired, revoked, or invalid credentials. A cookie-session request without a valid signed-in account returns authentication_required.' headers: WWW-Authenticate: schema: type: string content: application/json: schema: $ref: '#/components/schemas/Problem' examples: stableError: $ref: '#/components/examples/ProblemResponse' InsufficientScope: description: "The token lacks the operation's required scope" headers: WWW-Authenticate: schema: type: string content: application/json: schema: $ref: '#/components/schemas/Problem' examples: stableError: $ref: '#/components/examples/ProblemResponse' RateLimited: description: 'Per-token minute or daily plan limit exceeded' headers: Retry-After: schema: type: integer RateLimit-Limit: schema: type: integer RateLimit-Remaining: schema: type: integer RateLimit-Reset: schema: type: integer RateLimit-Policy: schema: type: string content: application/json: schema: $ref: '#/components/schemas/Problem' examples: stableError: $ref: '#/components/examples/ProblemResponse' PushTestRateLimited: description: 'Push-test account, source-IP, or outstanding-work allowance exhausted' headers: Retry-After: schema: type: integer content: application/json: schema: $ref: '#/components/schemas/Problem' examples: stableError: $ref: '#/components/examples/ProblemResponse' Problem: description: 'Request failed' content: application/json: schema: $ref: '#/components/schemas/Problem' examples: stableError: $ref: '#/components/examples/ProblemResponse' examples: AliasClaimRequest: summary: 'Claim a reserved Premium alias with a self-destruct timer' value: aliasId: 142 reservationToken: rsv_01JX8M6Q9SK4T4S6J40EHVA57Y label: 'Checkout tests' note: 'Burn after the release candidate closes' selfDestructAt: '2026-09-07T16:30:00Z' AliasClaimResponse: summary: 'Claimed alias' value: id: 142 address: maple-river@aliasbin.com localPart: maple-river domain: id: 3 name: aliasbin.com state: active style: words status: active label: 'Checkout tests' note: 'Burn after the release candidate closes' receivedCount: 0 rejectedCount: 0 unreadCount: 0 storageUsedBytes: 0 pushEnabled: false lastReceivedAt: null claimedAt: '2026-08-31T14:00:00+00:00' pausedAt: null burnedAt: null planPaused: false selfDestructAt: '2026-09-07T16:30:00+00:00' selfDestructSuspended: false expiringNotifiedAt: null domainTier: premium_only role: owner shared: false ownerHandle: null guestCount: 0 trash: enabled: true retentionDays: 30 MessagePageResponse: summary: 'A cursor page containing one received SMTP message' value: items: - id: 731 source: smtp alias: id: 142 address: maple-river@aliasbin.com label: 'Checkout tests' domain: aliasbin.com sender: name: 'Example Accounts' address: verify@example.test subject: 'Confirm your account' snippet: 'Your verification code is 482911.' receivedAt: '2026-08-31T14:02:19+00:00' expiresAt: '2026-10-30T14:02:19+00:00' sizeBytes: 1894 isRead: false unread: true starred: false pinned: false archived: false trashed: false trashedAt: null trashPurgeAt: null hasAttachments: false spamScore: -0.2 auth: spf: pass dkim: pass dmarc: pass otpCodes: - '482911' tags: [] processingStatus: processed nextCursor: null total: 1 MessageDetailResponse: summary: 'Parsed message detail with safe body links' value: id: 731 source: smtp alias: id: 142 address: maple-river@aliasbin.com label: 'Checkout tests' domain: aliasbin.com sender: name: 'Example Accounts' address: verify@example.test subject: 'Confirm your account' snippet: 'Your verification code is 482911.' receivedAt: '2026-08-31T14:02:19+00:00' expiresAt: '2026-10-30T14:02:19+00:00' sizeBytes: 1894 isRead: false unread: true starred: false pinned: false archived: false trashed: false trashedAt: null trashPurgeAt: null hasAttachments: false spamScore: -0.2 auth: spf: pass dkim: pass dmarc: pass otpCodes: - '482911' tags: [] processingStatus: processed headers: message-id: '' textBody: 'Your verification code is 482911.' htmlUrl: /api/v1/messages/731/html remoteImages: blockedUrl: '/api/v1/messages/731/html?remote=0' loadUrl: '/api/v1/messages/731/html?remote=1' alwaysLoadFromSender: false rawUrl: /api/v1/messages/731/raw verificationLinks: - 'https://example.test/verify?t=public-example' attachments: [] MailboxExportRequest: summary: 'Export one alias as a ZIP of EML files' value: aliasId: 142 MailboxExportResponse: summary: 'Queued mailbox export' value: id: 88 purpose: mailbox aliasId: 142 status: pending TestMessageRequest: summary: 'Inject deterministic verification mail into the normal parser' value: aliasId: 142 from: 'CI Runner ' subject: 'Checkout verification' text: 'Your verification code is 482911.' otp: '482911' attachments: [] TestMessageResponse: summary: 'Accepted seven-day test message' value: id: 732 aliasId: 142 alias: maple-river@aliasbin.com source: test processingStatus: stub receivedAt: '2026-08-31T14:10:00Z' expiresAt: '2026-09-07T14:10:00Z' WebhookCreateRequest: summary: 'Subscribe one endpoint to messages carrying an OTP' value: name: 'Verification worker' url: 'https://hooks.example.test/junkmail' subscription: mode: otp spki_pin: sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= include_text_body: true include_message_restored: false WebhookCreateResponse: summary: 'Endpoint metadata and its one-time HMAC secret' value: webhook: id: 17 name: 'Verification worker' url: 'https://hooks.example.test/junkmail' subscription: mode: otp spkiPin: sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= includeTextBody: true includeMessageRestored: false status: active failureCount: 0 secretVersion: 1 lastSuccessAt: null lastFailureAt: null failureSince: null disabledAt: null createdAt: '2026-08-31T14:12:00+00:00' updatedAt: '2026-08-31T14:12:00+00:00' secret: whsec_public_documentation_example secretShownOnce: true WebhookMessageReceived: summary: 'Signed message.received request body' value: id: evt_01J6N8XQ8G7K2M3P4R5S6T7V8W type: message.received time: '2026-08-31T14:02:19Z' api_version: '2026-08-31' shared_mailbox: false owner_handle: null data: alias: id: 142 address: maple-river@aliasbin.com label: 'Checkout tests' message_id: 731 from: address: verify@example.test name: 'Example Accounts' subject: 'Confirm your account' snippet: 'Your verification code is 482911.' otp: code: '482911' links: - 'https://example.test/verify?t=public-example' auth: spf: pass dkim: pass dmarc: pass spam_score: -0.2 size: 1894 has_attachments: false source: smtp received_at: '2026-08-31T14:02:19Z' url: /mailbox/messages/731 text_body: 'Your verification code is 482911.' ProblemResponse: summary: 'Stable machine-readable validation error' value: error: code: validation_failed message: 'The request contains invalid fields.' fields: url: https_required schemas: DeskSentMail: type: object required: - id - recipient - subject - body - status - sent_at properties: id: type: integer minimum: 1 recipient: type: string format: email subject: type: string body: type: string description: 'Plaintext; render as text only' status: type: string enum: - pending - failed - sent sent_at: type: string Problem: type: object required: - error properties: error: type: object required: - code - message properties: code: type: string message: type: string fields: type: object additionalProperties: true PlanLimits: type: object description: 'Resolved plan entitlements for the account, including per-account overrides.' required: - code - name - activeAliasCap - aliasLifetimeCap - claimsPerDay - candidateShufflesPerHour - candidateShufflesUnlimited - storageQuotaBytes - retentionDays - maxPinned - earlyAccessDomains - mailRuleCount - selfDestructTimers - bulkExport - developerAccess - apiTokenCount - webhookCount - testMessagesPerDay additionalProperties: false properties: code: type: string enum: - free - premium - pro name: type: string activeAliasCap: type: integer aliasLifetimeCap: type: integer claimsPerDay: type: integer candidateShufflesPerHour: type: integer candidateShufflesUnlimited: type: boolean storageQuotaBytes: type: integer minimum: 0 retentionDays: type: integer maxPinned: type: integer earlyAccessDomains: type: boolean mailRuleCount: type: integer selfDestructTimers: type: boolean bulkExport: type: boolean developerAccess: type: boolean apiTokenCount: type: integer webhookCount: type: integer testMessagesPerDay: type: integer MailboxDensity: type: string description: 'How much vertical room one message takes in the list.' enum: - default - comfortable - compact Account: type: object required: - id - email - storageUsedBytes - storageQuotaBytes - aliasesLifetimeClaimed - activeAliasCount - mailboxDensity - plan additionalProperties: false properties: id: type: integer minimum: 1 email: type: string format: email storageUsedBytes: type: integer minimum: 0 storageQuotaBytes: type: integer minimum: 0 aliasesLifetimeClaimed: type: integer minimum: 0 activeAliasCount: type: integer minimum: 0 mailboxDensity: $ref: '#/components/schemas/MailboxDensity' plan: $ref: '#/components/schemas/PlanLimits' isAdmin: type: boolean description: 'Present only in cookie-session GET /bootstrap responses.' handle: type: - string - 'null' description: 'Present only in cookie-session GET /bootstrap responses. The pseudonymous handle other accounts see on a shared mailbox, or null until the account takes part in sharing.' StorageUsage: type: object required: - usedBytes - quotaBytes - remainingBytes - usageRatio additionalProperties: false properties: usedBytes: type: integer minimum: 0 quotaBytes: type: integer minimum: 0 remainingBytes: type: integer minimum: 0 usageRatio: type: number minimum: 0 maximum: 1 description: 'usedBytes divided by quotaBytes, capped at 1; a zero quota reports 1.' BootstrapResponse: type: object description: 'Bearer responses contain only user, aliases, domains, and tags. Cookie-session responses add user.isAdmin, user.handle, csrfToken, mercure, and push.' required: - user - aliases - domains - tags additionalProperties: false properties: user: $ref: '#/components/schemas/Account' aliases: type: array items: $ref: '#/components/schemas/Alias' domains: type: array items: $ref: '#/components/schemas/Domain' tags: type: array items: $ref: '#/components/schemas/TagWithCount' csrfToken: type: string description: 'Cookie-session only; send it as X-CSRF-Token on state changes.' mercure: type: object description: 'Cookie-session only; realtime hub details for the browser mailbox.' required: - hubUrl - topic additionalProperties: false properties: hubUrl: type: string format: uri topic: type: string format: uri push: $ref: '#/components/schemas/PushSettings' Tag: type: object required: - id - name - color properties: id: type: integer minimum: 1 name: type: string minLength: 1 maxLength: 64 color: type: - string - 'null' pattern: '^#[0-9A-F]{6}$' description: 'Uppercase six-digit hexadecimal color, or null when unset.' TagWithCount: allOf: - $ref: '#/components/schemas/Tag' - type: object required: - messageCount properties: messageCount: type: integer minimum: 0 Domain: type: object required: - id - name - state - tier - claimable - upgradeRequired additionalProperties: false properties: id: type: integer name: type: string state: type: string enum: - active - retired tier: type: string enum: - public - premium_only claimable: type: boolean description: 'True when the current account may claim a new alias on this domain.' upgradeRequired: type: boolean Alias: type: object required: - id - address - localPart - domain - style - status - label - note - receivedCount - rejectedCount - unreadCount - storageUsedBytes - pushEnabled - lastReceivedAt - claimedAt - pausedAt - burnedAt - planPaused - selfDestructAt - selfDestructSuspended - expiringNotifiedAt - domainTier - role - shared - ownerHandle - guestCount - trash additionalProperties: false properties: canSend: type: boolean description: 'This account owns an active protected operator desk.' id: type: integer minimum: 1 address: type: string format: email localPart: type: string domain: type: object required: - id - name - state additionalProperties: false properties: id: type: integer minimum: 1 name: type: string state: type: string enum: - active - closed - draining - retired style: type: string enum: - words - pronounceable - numeric status: type: string enum: - active - paused - burned label: type: - string - 'null' note: type: - string - 'null' receivedCount: type: integer minimum: 0 rejectedCount: type: integer minimum: 0 unreadCount: type: integer minimum: 0 storageUsedBytes: type: integer minimum: 0 pushEnabled: type: boolean description: "Your own browser push notifications for this mailbox; see PUT /push/aliases/{id}. On a mailbox shared with you this is your preference for that one mailbox and never the owner's, which governs the owner's devices alone." lastReceivedAt: type: - string - 'null' format: date-time claimedAt: type: - string - 'null' format: date-time pausedAt: type: - string - 'null' format: date-time burnedAt: type: - string - 'null' format: date-time planPaused: type: boolean description: 'True when a downgrade paused this alias to enforce the active-alias cap.' selfDestructAt: type: - string - 'null' format: date-time selfDestructSuspended: type: boolean description: 'A preserved timer suspended after entitlement loss; it never burns while suspended.' expiringNotifiedAt: type: - string - 'null' format: date-time domainTier: type: string enum: - public - premium_only role: type: string description: "How this account reaches the mailbox. A guest never sees the owner's note, storage or timer state." enum: - owner - collaborator - viewer shared: type: boolean description: 'True when another account owns this mailbox and shared it with you.' ownerHandle: type: - string - 'null' description: 'Pseudonymous handle of the account that owns a shared mailbox. Null on a mailbox you own. Real addresses never cross an account boundary.' guestCount: type: integer minimum: 0 description: 'How many accounts you have given access to this mailbox. Always 0 on a mailbox somebody else owns: a guest is never told who else is in the room.' trash: type: object description: "What deleting from this mailbox does. It is the *owner's* plan, on a shared mailbox as much as on your own, because that is the plan the delete follows - your own tells you nothing about somebody else's mailbox." required: - enabled - retentionDays additionalProperties: false properties: enabled: type: boolean description: 'False means deleting from this mailbox is permanent and immediate; there is nothing to restore and no undo to offer.' retentionDays: type: integer minimum: 0 description: 'Days a deleted message stays recoverable. 0 when enabled is false.' ShareGrant: type: object description: 'One guest webhook or guest API token pointed at a mailbox you own.' required: - kind - webhookId - apiTokenId - guestHandle - endpointHost - createdAt - revokedAt - revokedBy - isNewToOwner - deliveryStatus - lastSuccessAt - lastFailureAt - lastUsedAt additionalProperties: false properties: kind: type: string enum: - webhook - api_token webhookId: type: - integer - 'null' minimum: 1 apiTokenId: type: - integer - 'null' minimum: 1 guestHandle: type: - string - 'null' description: 'Pseudonymous handle of the guest. Real addresses never cross an account boundary.' endpointHost: type: - string - 'null' description: 'Host of a guest webhook endpoint. The path is never shown, because it can carry a secret.' createdAt: type: - string - 'null' format: date-time revokedAt: type: - string - 'null' format: date-time revokedBy: type: - string - 'null' enum: - owner - guest - system - null isNewToOwner: type: boolean description: 'True until acknowledged; raises the notice shown when a guest first registers a webhook on your alias.' deliveryStatus: type: - string - 'null' description: 'Current state of a guest webhook endpoint.' enum: - active - paused - disabled - null lastSuccessAt: type: - string - 'null' format: date-time lastFailureAt: type: - string - 'null' format: date-time lastUsedAt: type: - string - 'null' format: date-time description: 'Last use of a guest API token, across every request that token made.' RevokedShareGrant: type: object description: 'One reason your access to a shared mailbox stopped: one grant the owner refused, or the share itself suspended or ended.' required: - aliasId - aliasAddress - ownerHandle - reason - reversible - kind - webhookId - apiTokenId - endpointHost - endedAt - revokedAt - revokedBy additionalProperties: false properties: aliasId: type: integer minimum: 1 description: 'The mailbox. Always present, so a client can match its own records even once the address is gone.' aliasAddress: type: - string - 'null' format: email description: 'Null once access has stopped for the whole mailbox: a suspended or ended share takes the address with it.' ownerHandle: type: - string - 'null' description: 'Null once access has stopped for the whole mailbox.' reason: type: string description: 'owner_revoked: the owner ended this one endpoint or token and you keep the mailbox. share_suspended: the share is not granting anything at the moment and may resume on its own. share_ended: your access to the mailbox is over; which of the several ways it ended is deliberately not reported. guest_left: you gave the mailbox up yourself.' enum: - owner_revoked - share_suspended - share_ended - guest_left reversible: type: boolean description: 'True when access can return without anybody granting it again. Prefer this over reason when deciding whether to retry, so an unfamiliar future reason still behaves.' kind: type: string description: 'share means the whole mailbox stopped and every grant you held on it stopped with it, so webhookId, apiTokenId and endpointHost are all null.' enum: - webhook - api_token - share webhookId: type: - integer - 'null' minimum: 1 apiTokenId: type: - integer - 'null' minimum: 1 endpointHost: type: - string - 'null' endedAt: type: - string - 'null' format: date-time description: 'When access stopped. Null on a suspension whose start was not recorded.' revokedAt: type: - string - 'null' format: date-time description: 'Same as endedAt, except on a suspension, where nothing was revoked and this is null.' revokedBy: type: - string - 'null' description: 'Only ever owner, and only on an owner_revoked grant. Null on a share, where naming the revoker would say how the share ended.' enum: - owner - guest - system - null AliasTimer: type: object required: - id - selfDestructAt - selfDestructSuspended - expiringNotifiedAt additionalProperties: false properties: id: type: integer minimum: 1 selfDestructAt: type: - string - 'null' format: date-time selfDestructSuspended: type: boolean expiringNotifiedAt: type: - string - 'null' format: date-time CandidateReservation: type: object required: - reservationToken - expiresAt - candidates properties: reservationToken: type: string expiresAt: type: string format: date-time candidates: type: array items: type: object required: - id - localPart - domain - address properties: id: type: integer localPart: type: string domain: type: string address: type: string format: email CandidateRelease: type: object required: - released additionalProperties: false properties: released: type: integer minimum: 0 description: 'Reserved candidates returned to the pool.' MessageSummary: type: object required: - id - source - alias - sender - subject - snippet - receivedAt - expiresAt - sizeBytes - isRead - unread - starred - pinned - archived - trashed - trashedAt - trashPurgeAt - hasAttachments - spamScore - auth - otpCodes - tags - processingStatus additionalProperties: true properties: id: type: integer minimum: 1 source: type: string enum: - smtp - test alias: type: object required: - id - address - label - domain additionalProperties: false properties: id: type: integer minimum: 1 address: type: string format: email label: type: - string - 'null' domain: type: string sender: type: object required: - name - address additionalProperties: false properties: name: type: - string - 'null' address: type: string format: email subject: type: string snippet: type: string receivedAt: type: string format: date-time expiresAt: type: string format: date-time description: 'Retention deadline; test messages always expire seven days after injection.' sizeBytes: type: integer minimum: 0 isRead: type: boolean unread: type: boolean description: 'Always the inverse of isRead.' starred: type: boolean pinned: type: boolean archived: type: boolean trashed: type: boolean description: 'In the trash. Trashed mail is left out of every list, count and search unless trashed=true is asked for.' trashedAt: type: - string - 'null' format: date-time description: 'When the message was deleted into the trash, or null when it is not in the trash.' trashPurgeAt: type: - string - 'null' format: date-time description: 'When the trash destroys it: the earlier of expiresAt and the plan trash window, fixed at delete time so a later downgrade cannot bring it forward.' hasAttachments: type: boolean spamScore: type: - number - 'null' auth: type: object required: - spf - dkim - dmarc additionalProperties: false properties: spf: type: - string - 'null' dkim: type: - string - 'null' dmarc: type: - string - 'null' otpCodes: type: array maxItems: 3 description: 'Numbers found in the message that could be verification codes, in the order they appear and at most three. They are not ranked and not filtered: an order number, a postal code or an invoice total appears here as readily as a code. The first entry is the same value the message.otp_extracted webhook reports as otp.code.' items: type: string tags: type: array items: $ref: '#/components/schemas/Tag' processingStatus: type: string enum: - stub - processing - processed - failed MessageState: type: object required: - isRead - starred - pinned - archived - tags additionalProperties: false properties: isRead: type: boolean starred: type: boolean pinned: type: boolean archived: type: boolean tags: type: array items: $ref: '#/components/schemas/Tag' BulkMessageResult: type: object required: - updated - deleted - trashed - restored - outcomes additionalProperties: false properties: updated: type: integer minimum: 0 description: 'Messages whose state actually changed; messages already in the requested state are not counted.' deleted: type: integer minimum: 0 description: 'Messages that left the mailbox, whether they went to the trash or were destroyed.' trashed: type: integer minimum: 0 description: 'The recoverable part of deleted. Zero on a plan without a trash and for delete_forever.' restored: type: integer minimum: 0 description: 'Messages taken back out of the trash. Messages that were not in it are not counted.' outcomes: type: array description: 'One entry per requested identifier for delete, delete_forever and restore, saying where each message now is. Empty for the state and tag actions, whose aggregate counts already describe them. A request spanning mailboxes with different owners can only be undone from this list, because the counts do not say which identifiers were destroyed.' items: $ref: '#/components/schemas/MessageOutcome' MessageOutcome: type: object description: 'Where one message is now, not what the request did to it: a repeated delete answers trashed with the purge date it already had.' required: - id - outcome - trashPurgeAt additionalProperties: false properties: id: type: integer minimum: 1 outcome: type: string enum: - trashed - deleted - restored trashPurgeAt: type: - string - 'null' format: date-time description: 'When the trash destroys it; null unless the outcome is trashed.' EmptyTrashResult: type: object required: - deleted - remaining additionalProperties: false properties: deleted: type: integer minimum: 0 description: 'Messages destroyed by this request, at most 1000.' remaining: type: integer minimum: 0 description: 'Messages still in the trash afterwards. Repeat the request while this is above zero.' RemoteImagePreference: type: object required: - sender - alwaysLoad additionalProperties: false properties: sender: type: string format: email description: 'Normalized lowercase sender address the preference applies to.' alwaysLoad: type: boolean MailboxExport: type: object description: 'List and detail responses use snake_case alias_id plus PostgreSQL timestamptz text such as 2026-08-31 14:12:00+00 (not RFC 3339); the queue response uses camelCase aliasId.' required: - id - purpose - status additionalProperties: false properties: id: type: integer minimum: 1 purpose: const: mailbox aliasId: type: - integer - 'null' minimum: 1 description: 'Queue (POST) responses only.' alias_id: type: - integer - 'null' minimum: 1 description: 'List and detail responses only.' alias: type: - string - 'null' format: email status: type: string enum: - pending - processing - ready - failed - expired error: type: - string - 'null' ready_at: type: - string - 'null' expires_at: type: - string - 'null' created_at: type: string downloadUrl: type: - string - 'null' description: 'Detail responses only; set once the export is ready.' TestMessageInput: type: object required: - aliasId - from - subject additionalProperties: false anyOf: - required: - text - required: - html properties: aliasId: type: integer minimum: 1 from: type: string description: 'RFC 5322 mailbox such as CI runner .' subject: type: string minLength: 1 maxLength: 998 text: type: - string - 'null' maxLength: 2097152 html: type: - string - 'null' maxLength: 2097152 otp: type: - string - 'null' maxLength: 128 attachments: type: array maxItems: 10 items: type: object required: - filename - contentBase64 additionalProperties: false properties: filename: type: string minLength: 1 maxLength: 255 contentType: type: string default: application/octet-stream contentBase64: type: string contentEncoding: base64 description: 'Combined decoded attachment bytes may not exceed 1 MiB.' InjectedTestMessage: type: object required: - id - aliasId - alias - source - processingStatus - receivedAt - expiresAt additionalProperties: false properties: id: type: integer minimum: 1 aliasId: type: integer minimum: 1 alias: type: string format: email source: const: test processingStatus: type: string enum: - stub receivedAt: type: string format: date-time expiresAt: type: string format: date-time MessagePage: type: object required: - items - nextCursor - total properties: items: type: array items: $ref: '#/components/schemas/MessageSummary' nextCursor: type: - string - 'null' total: type: integer minimum: 0 MessageDetail: allOf: - $ref: '#/components/schemas/MessageSummary' - type: object required: - headers - textBody - htmlUrl - remoteImages - rawUrl - verificationLinks - attachments properties: headers: type: object description: 'Parsed message headers keyed by lowercase header name.' additionalProperties: true textBody: type: - string - 'null' htmlUrl: type: - string - 'null' remoteImages: type: - object - 'null' description: 'Null when the message has no HTML body.' required: - blockedUrl - loadUrl - alwaysLoadFromSender additionalProperties: false properties: blockedUrl: type: string loadUrl: type: string alwaysLoadFromSender: type: boolean rawUrl: type: string verificationLinks: type: array maxItems: 3 description: 'Verification links found in the message, best first and at most three. Footer links — help centre, contact, unsubscribe, preferences, app stores and social networks — are excluded.' items: type: string format: uri attachments: type: array items: type: object required: - id - filename - mimeType - sizeBytes - dangerous - downloadUrl - previewUrl additionalProperties: false properties: id: type: integer minimum: 1 filename: type: string mimeType: type: string sizeBytes: type: integer minimum: 0 dangerous: type: boolean downloadUrl: type: string previewUrl: type: - string - 'null' PushSettings: type: object required: - enabled - nudgeDismissed - publicKey - subscriptions additionalProperties: false properties: enabled: type: boolean nudgeDismissed: type: boolean description: 'True once the account has declined the one-time browser-notification offer; see DELETE /push/nudge.' publicKey: type: - string - 'null' description: 'VAPID application server key, or null when push is not configured.' subscriptions: type: array items: type: object required: - id - endpointHash - createdAt - lastSuccessAt additionalProperties: false properties: id: type: integer minimum: 1 endpointHash: type: string pattern: '^[0-9a-f]{64}$' description: "SHA-256 of this subscription's own push endpoint, so a browser can recognise its own registration. The endpoint itself is never returned." createdAt: type: string description: 'PostgreSQL timestamptz text such as 2026-08-31 14:12:00.482911+00 (not RFC 3339).' lastSuccessAt: type: - string - 'null' description: 'PostgreSQL timestamptz text such as 2026-08-31 14:12:00.482911+00 (not RFC 3339).' PushSubscription: type: object required: - id - enabled additionalProperties: false properties: id: type: integer minimum: 1 enabled: type: boolean Webhook: type: object required: - id - name - url - subscription - spkiPin - includeTextBody - includeMessageRestored - status - failureCount - secretVersion - lastSuccessAt - lastFailureAt - failureSince - disabledAt - createdAt - updatedAt additionalProperties: false properties: id: type: integer minimum: 1 name: type: string maxLength: 80 url: type: string format: uri subscription: $ref: '#/components/schemas/WebhookSubscription' spkiPin: type: - string - 'null' pattern: '^sha256/[A-Za-z0-9+/]{43}=$' includeTextBody: type: boolean includeMessageRestored: type: boolean description: 'Whether this endpoint also receives message.restored when a message is recovered from the trash.' status: type: string enum: - active - paused - disabled description: 'disabled is applied automatically after three consecutive days of failures.' failureCount: type: integer minimum: 0 secretVersion: type: integer minimum: 1 lastSuccessAt: type: - string - 'null' format: date-time lastFailureAt: type: - string - 'null' format: date-time failureSince: type: - string - 'null' format: date-time disabledAt: type: - string - 'null' format: date-time createdAt: type: string format: date-time updatedAt: type: string format: date-time WebhookList: type: object required: - items - limit additionalProperties: false properties: items: type: array items: $ref: '#/components/schemas/Webhook' limit: type: integer minimum: 0 description: 'Maximum webhook endpoints allowed by the current plan.' WebhookWithSecret: type: object required: - webhook - secret - secretShownOnce additionalProperties: false properties: webhook: $ref: '#/components/schemas/Webhook' secret: type: string description: 'Plaintext HMAC signing secret; it is never returned again.' secretShownOnce: const: true WebhookDeliveryAttempt: type: object required: - attempt - status - httpStatus - errorCode - latencyMs - responseSnippet - attemptedAt additionalProperties: false properties: attempt: type: integer minimum: 1 status: type: string enum: - succeeded - failed httpStatus: type: - integer - 'null' errorCode: type: - string - 'null' latencyMs: type: integer minimum: 0 responseSnippet: type: - string - 'null' attemptedAt: type: string format: date-time WebhookDelivery: type: object description: 'Delivery-log entry. nextAttemptAt, lastAttemptAt, deliveredAt, and createdAt are PostgreSQL timestamptz text such as 2026-08-31 14:12:00.482911+00 (not RFC 3339); attempts[].attemptedAt is RFC 3339 UTC.' required: - id - webhookId - webhookName - eventId - eventType - status - attemptCount - cycleAttemptCount - lastHttpStatus - lastError - latencyMs - responseSnippet - nextAttemptAt - lastAttemptAt - deliveredAt - createdAt - attempts additionalProperties: false properties: id: type: integer minimum: 1 webhookId: type: integer minimum: 1 webhookName: type: string eventId: type: string pattern: '^evt_[0-9A-HJKMNP-TV-Z]{26}$' eventType: type: string enum: - message.received - message.otp_extracted - alias.claimed - alias.paused - alias.burned - alias.expiring - webhook.test status: type: string enum: - pending - delivering - succeeded - failed - abandoned attemptCount: type: integer minimum: 0 cycleAttemptCount: type: integer minimum: 0 description: 'Attempts in the current retry cycle; manual redelivery resets it while attemptCount keeps growing.' lastHttpStatus: type: - integer - 'null' lastError: type: - string - 'null' latencyMs: type: - integer - 'null' responseSnippet: type: - string - 'null' nextAttemptAt: type: string lastAttemptAt: type: - string - 'null' deliveredAt: type: - string - 'null' createdAt: type: string attempts: type: array items: $ref: '#/components/schemas/WebhookDeliveryAttempt' WebhookDeliveryPage: type: object required: - items additionalProperties: false properties: items: type: array description: 'Newest first, bounded by the limit parameter.' items: $ref: '#/components/schemas/WebhookDelivery' WebhookDeliveryReceipt: type: object required: - id - webhookId - eventId - eventType - status - attemptCount - cycleAttemptCount - nextAttemptAt - createdAt additionalProperties: false properties: id: type: integer minimum: 1 webhookId: type: integer minimum: 1 eventId: type: string pattern: '^evt_[0-9A-HJKMNP-TV-Z]{26}$' eventType: type: string enum: - message.received - message.otp_extracted - alias.claimed - alias.paused - alias.burned - alias.expiring - webhook.test status: type: string enum: - pending - delivering - succeeded - failed - abandoned attemptCount: type: integer minimum: 0 cycleAttemptCount: type: integer minimum: 0 nextAttemptAt: type: string format: date-time createdAt: type: string format: date-time WebhookDeliveryQueued: type: object required: - delivery additionalProperties: false properties: delivery: $ref: '#/components/schemas/WebhookDeliveryReceipt' WebhookSubscription: oneOf: - type: object required: - mode additionalProperties: false properties: mode: const: all - type: object required: - mode - alias_ids additionalProperties: false properties: mode: const: aliases alias_ids: type: array minItems: 1 maxItems: 100 uniqueItems: true items: type: integer minimum: 1 - type: object required: - mode - label additionalProperties: false properties: mode: const: label label: type: string minLength: 1 maxLength: 160 - type: object required: - mode - tag_id additionalProperties: false properties: mode: const: tag tag_id: type: integer minimum: 1 - type: object required: - mode additionalProperties: false properties: mode: const: otp WebhookAlias: type: object required: - id - address - label additionalProperties: false properties: id: type: integer minimum: 1 address: type: string format: email label: type: - string - 'null' WebhookOtp: type: object required: - code - links additionalProperties: false properties: code: type: - string - 'null' description: 'The first number in the message that could be a verification code, or null when the message holds none. Candidates are every run of four to eight digits, in the order they appear; the message wording around them is not read, so an order number, a postal code, a price or a calendar year can appear here instead of a code. Treat this as a hint to a human reader rather than as a value to act on automatically. Ranking these by how the message presents them is planned.' links: type: array maxItems: 3 description: 'The verification links the message asks the reader to open, best first and at most three. Help-centre, contact, unsubscribe, preference, app-store and social links are excluded.' items: type: string format: uri WebhookEvent: oneOf: - $ref: '#/components/schemas/WebhookMessageReceived' - $ref: '#/components/schemas/WebhookOtpExtracted' - $ref: '#/components/schemas/WebhookAliasEvent' - $ref: '#/components/schemas/WebhookTestEvent' WebhookMessageReceived: type: object required: - id - type - time - api_version - shared_mailbox - owner_handle - data additionalProperties: false description: 'Metadata and OTP event. text_body appears only after an endpoint opt-in and is sanitized/truncated to 32 KiB. HTML, headers, raw MIME, and attachments are never inlined.' properties: id: type: string pattern: '^evt_[0-9A-HJKMNP-TV-Z]{26}$' type: const: message.received time: type: string format: date-time api_version: type: string example: '2026-08-31' shared_mailbox: type: boolean description: 'True when this event came from a mailbox another account owns and shared with you. Additive within api_version 2026-08-31.' owner_handle: type: - string - 'null' description: 'Pseudonymous handle of the mailbox owner when shared_mailbox is true, otherwise null. Real addresses never cross an account boundary.' data: type: object required: - alias - message_id - from - subject - snippet - otp - auth - spam_score - size - has_attachments - source - received_at - url additionalProperties: false properties: alias: $ref: '#/components/schemas/WebhookAlias' message_id: type: integer minimum: 1 from: type: object required: - address - name additionalProperties: false properties: address: type: string format: email name: type: - string - 'null' subject: type: string snippet: type: string otp: $ref: '#/components/schemas/WebhookOtp' auth: type: object required: - spf - dkim - dmarc additionalProperties: false properties: spf: type: - string - 'null' dkim: type: - string - 'null' dmarc: type: - string - 'null' spam_score: type: - number - 'null' size: type: integer minimum: 0 has_attachments: type: boolean source: type: string enum: - smtp - test received_at: type: string format: date-time url: type: string format: uri-reference text_body: type: string maxLength: 32768 WebhookOtpExtracted: type: object required: - id - type - time - api_version - shared_mailbox - owner_handle - data additionalProperties: false properties: id: type: string pattern: '^evt_[0-9A-HJKMNP-TV-Z]{26}$' type: const: message.otp_extracted time: type: string format: date-time api_version: type: string shared_mailbox: type: boolean description: 'True when this event came from a mailbox another account owns and shared with you. Additive within api_version 2026-08-31.' owner_handle: type: - string - 'null' description: 'Pseudonymous handle of the mailbox owner when shared_mailbox is true, otherwise null. Real addresses never cross an account boundary.' data: type: object required: - alias - message_id - otp - source - received_at - url additionalProperties: false properties: alias: $ref: '#/components/schemas/WebhookAlias' message_id: type: integer minimum: 1 otp: $ref: '#/components/schemas/WebhookOtp' source: type: string enum: - smtp - test received_at: type: string format: date-time url: type: string format: uri-reference WebhookAliasEvent: type: object required: - id - type - time - api_version - shared_mailbox - owner_handle - data additionalProperties: false properties: id: type: string pattern: '^evt_[0-9A-HJKMNP-TV-Z]{26}$' type: type: string enum: - alias.claimed - alias.paused - alias.burned - alias.expiring time: type: string format: date-time api_version: type: string shared_mailbox: type: boolean description: 'True when this event came from a mailbox another account owns and shared with you. Additive within api_version 2026-08-31.' owner_handle: type: - string - 'null' description: 'Pseudonymous handle of the mailbox owner when shared_mailbox is true, otherwise null. Real addresses never cross an account boundary.' data: type: object required: - alias additionalProperties: false properties: alias: $ref: '#/components/schemas/WebhookAlias' claimed_at: type: string format: date-time paused_at: type: string format: date-time burned_at: type: string format: date-time self_destruct_at: type: string format: date-time WebhookTestEvent: type: object required: - id - type - time - api_version - shared_mailbox - owner_handle - data additionalProperties: false properties: id: type: string pattern: '^evt_[0-9A-HJKMNP-TV-Z]{26}$' type: const: webhook.test time: type: string format: date-time api_version: type: string shared_mailbox: type: boolean description: 'True when this event came from a mailbox another account owns and shared with you. Additive within api_version 2026-08-31.' owner_handle: type: - string - 'null' description: 'Pseudonymous handle of the mailbox owner when shared_mailbox is true, otherwise null. Real addresses never cross an account boundary.' data: type: object required: - endpoint_id - message additionalProperties: false properties: endpoint_id: type: integer minimum: 1 message: type: string