# API reference

> Every REST endpoint and object, generated from the OpenAPI file.

Source: https://developer.mueslimail.com/docs/api-reference/

Generated from `openapi/openapi.yaml` (version 0.1.0) at build time. Base URL: `https://api.mueslimail.com`. The API also serves the file itself at `https://api.mueslimail.com/openapi.yaml`.

Every operation answers on `/v1`, the current version, and on the deprecated `/v0` alias with the same bodies and status codes. See [Versions and SDKs](https://developer.mueslimail.com/docs/versions.md) for the deprecation headers, the policy and the official clients. The paths below show the `/v0` prefix used by the file; replace it with `/v1` and nothing else changes.

## Endpoints

### GET /v0/auth/me

Caller summary (kept for older clients; same body as GET /v0/me)

Responses:

* `200` OK: Me
* `401` Error envelope: Error

### GET /v0/me

The authenticated caller, a person's session or an API key

Responses:

* `200` OK: Me
* `401` Error envelope: Error

### GET /v0/auth/providers

Which sign in methods this deployment can complete (public)

Responses:

* `200` OK: object

### POST /v0/auth/magic-link

Email a single use sign in link (public)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `email` | string (email) | required |
| `redirect_after` | string | Path on the site, for example /console/ |

Responses:

* `202` Sent (or accepted when the address has no account: same answer)
* `303` A form post is redirected back to the login page with sent=1
* `400` Error envelope: Error
* `429` Error envelope: Error
* `501` Error envelope: Error
* `502` Error envelope: Error

### POST /v0/auth/password/login

Sign in with an email address and a password (public)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `email` | string (email) | required |
| `password` | string | required |
| `redirect_after` | string |  |

Responses:

* `200` Session, or the second factor step (MFAStep): Session
* `303` A form post goes to the login page with the session in the fragment, or with ?error=<code>
* `401` Error envelope: Error
* `429` Error envelope: Error
* `501` Error envelope: Error

### POST /v0/auth/password/signup

Create an account with an email address and a password (public)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `email` | string (email) | required |
| `password` | string | required, max length 128 |
| `redirect_after` | string |  |

Responses:

* `202` Confirmation link sent (same answer for an existing account)
* `303` A form post is redirected back to the sign up page with sent=confirm
* `400` Error envelope: Error
* `429` Error envelope: Error
* `501` Error envelope: Error
* `502` Error envelope: Error

### GET /v0/auth/password

Whether the person signed in has a password

Responses:

* `200` OK: object
* `403` Error envelope: Error

### PUT /v0/auth/password

Set or change the password of the person signed in

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `current_password` | string |  |
| `new_password` | string | required, max length 128 |

Responses:

* `200` OK: object
* `400` Error envelope: Error
* `401` Error envelope: Error
* `403` Error envelope: Error

### GET /v0/auth/callback

Finish a sign in, with a magic link token or an OAuth code (public)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `token` | string | in query, Magic link token |
| `code` | string | in query, OAuth authorization code |
| `state` | string | in query, OAuth state, single use, 10 minutes |

Responses:

* `200` Session, or the second factor step (MFAStep) when a factor is still to be proved: Session
* `303` A browser goes to the login page with the session in the fragment, or with ?error=<code>
* `400` Error envelope: Error
* `401` Error envelope: Error
* `403` Error envelope: Error
* `502` Error envelope: Error

### GET /v0/auth/oauth/{provider}/start

Start Google, Microsoft or GitHub sign in with PKCE (public)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `provider` | google \| microsoft \| github | in path, required |
| `redirect` | 1 | in query, Send the browser to the provider instead of returning JSON |
| `redirect_after` | string | in query, Path on the site to return to |

Responses:

* `200` OK: OAuthStart
* `302` The browser goes to the provider
* `400` Error envelope: Error
* `501` Error envelope: Error

### POST /v0/auth/refresh

Rotate a refresh token into a new session (public)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `refresh_token` | string | required |

Responses:

* `200` Session: Session
* `400` Error envelope: Error
* `401` Error envelope: Error
* `501` Error envelope: Error

### POST /v0/auth/logout

End a session (public)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `refresh_token` | string |  |

Responses:

* `204` Ended (also when the token was already invalid)
* `400` Error envelope: Error
* `501` Error envelope: Error

### GET /v0/inboxes

List inboxes, newest first

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `limit` | integer | in query, min 1, max 100, default 20 |
| `page_token` | string | in query |

Responses:

* `200` OK: InboxList

### POST /v0/inboxes

Create an inbox (repeat client_id to get the same inbox)

Body (CreateInbox):

| Name | Type | Notes |
| --- | --- | --- |
| `username` | string | pattern ^[a-z0-9]([a-z0-9._-]{0,62}[a-z0-9])?$ |
| `domain` | string |  |
| `display_name` | string | max length 200 |
| `client_id` | string | pattern ^[A-Za-z0-9._~-]{1,256}$ |

Responses:

* `200` Existing inbox for this client_id: Inbox
* `201` Created: Inbox
* `403` Error envelope: Error
* `409` Error envelope: Error

### GET /v0/inboxes/{inbox}

Get an inbox

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |

Responses:

* `200` OK: Inbox
* `404` Error envelope: Error

### PATCH /v0/inboxes/{inbox}

Update the display name, the signature, or the support intake of an inbox

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `display_name` | string | max length 200 |
| `ticket_intake` | boolean | New threads on this inbox become tickets in the support queue. |
| `signature_html` | string | HTML signature of this address. Empty string removes it., max length 20000 |

Responses:

* `200` OK: Inbox

### DELETE /v0/inboxes/{inbox}

Delete an inbox and all its mail

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |

Responses:

* `204` Deleted
* `404` Error envelope: Error

### GET /v0/inboxes/{inbox}/threads

List threads of an inbox

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `limit` | integer | in query, min 1, max 100, default 20 |
| `page_token` | string | in query |
| `label` | string | in query |
| `include_review` | boolean | in query, Also return messages held for security review; needs message_review, default false |

Responses:

* `200` OK: ThreadList

### GET /v0/threads

List threads across the organization

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `limit` | integer | in query, min 1, max 100, default 20 |
| `page_token` | string | in query |
| `label` | string | in query |
| `include_review` | boolean | in query, Also return messages held for security review; needs message_review, default false |

Responses:

* `200` OK: ThreadList

### GET /v0/threads/{thread}

Get a thread with the messages the caller may see, oldest first

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `thread` | string (uuid) | in path, required |
| `include_review` | boolean | in query, Also return messages held for security review; needs message_review, default false |

Responses:

* `200` OK: Thread

### GET /v0/inboxes/{inbox}/messages

List message metadata (no bodies)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `limit` | integer | in query, min 1, max 100, default 20 |
| `page_token` | string | in query |
| `label` | string | in query |
| `include_review` | boolean | in query, Also return messages held for security review; needs message_review, default false |

Responses:

* `200` OK: MessageList

### POST /v0/inboxes/{inbox}/messages/send

Send a new message (new thread)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `Idempotency-Key` | string | in header, max length 256 |

Body (Send):

| Name | Type | Notes |
| --- | --- | --- |
| `to` | string (email)[] | required, at least 1 item |
| `cc` | string (email)[] |  |
| `bcc` | string (email)[] |  |
| `subject` | string |  |
| `text` | string |  |
| `html` | string |  |
| `labels` | string[] |  |
| `attachments` | Attachment[] |  |

Responses:

* `200` Accepted by the transport, or queued when the install has no transport: SendResult
* `403` Error envelope: Error
* `409` Error envelope: Error
* `422` Error envelope: Error
* `429` Error envelope: Error
* `502` email_delivery_failed: the transport did not accept the message, with delivery_error_code and delivery_error: Error
* `503` Error envelope: Error

### GET /v0/inboxes/{inbox}/messages/{message}

Get a message with bodies, extracted_text, authentication results and gatekeeper verdict

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `message` | string | in path, required, Message UUID or RFC Message-ID (URL encoded) |
| `include_review` | boolean | in query, Also return messages held for security review; needs message_review, default false |

Responses:

* `200` OK: Message
* `403` Error envelope: Error
* `404` Error envelope: Error

### PATCH /v0/inboxes/{inbox}/messages/{message}

Add or remove custom labels

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `message` | string | in path, required, Message UUID or RFC Message-ID (URL encoded) |

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `add_labels` | string[] |  |
| `remove_labels` | string[] |  |

Responses:

* `200` OK: Message

### GET /v0/inboxes/{inbox}/messages/{message}/raw

Raw RFC 822 message

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `message` | string | in path, required, Message UUID or RFC Message-ID (URL encoded) |
| `include_review` | boolean | in query, Also return messages held for security review; needs message_review, default false |

Responses:

* `200` OK: message/rfc822

### POST /v0/inboxes/{inbox}/messages/{message}/reply

Reply in the same thread

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `message` | string | in path, required, Message UUID or RFC Message-ID (URL encoded) |
| `Idempotency-Key` | string | in header, max length 256 |

Body (Reply):

| Name | Type | Notes |
| --- | --- | --- |
| `text` | string |  |
| `html` | string |  |
| `reply_all` | boolean |  |
| `labels` | string[] |  |
| `attachments` | Attachment[] |  |

Responses:

* `200` Accepted by the transport, or queued when the install has no transport: SendResult
* `403` Error envelope: Error
* `422` Error envelope: Error
* `429` Error envelope: Error
* `502` email_delivery_failed: the transport did not accept the reply, with delivery_error_code and delivery_error: Error
* `503` Error envelope: Error

### POST /v0/inboxes/{inbox}/messages/{message}/release

Release a message held for review (needs message_release, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `message` | string | in path, required, Message UUID or RFC Message-ID (URL encoded) |

Responses:

* `200` Released: Message
* `403` Error envelope: Error
* `404` Error envelope: Error
* `409` Error envelope: Error

### GET /v0/inboxes/{inbox}/messages/{message}/attachments/{attachment}

Download an attachment

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `message` | string | in path, required, Message UUID or RFC Message-ID (URL encoded) |
| `attachment` | string (uuid) | in path, required |
| `include_review` | boolean | in query, Also return messages held for security review; needs message_review, default false |

Responses:

* `200` Attachment bytes with its content type

### GET /v0/inboxes/{inbox}/drafts

List drafts, newest first (needs message_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `limit` | integer | in query, min 1, max 100, default 20 |
| `page_token` | string | in query |

Responses:

* `200` OK: DraftList

### POST /v0/inboxes/{inbox}/drafts

Create a draft (needs message_send)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |

Body (CreateDraft):

| Name | Type | Notes |
| --- | --- | --- |
| `to` | string (email)[] |  |
| `cc` | string (email)[] |  |
| `bcc` | string (email)[] |  |
| `subject` | string |  |
| `text` | string |  |
| `html` | string |  |
| `labels` | string[] |  |
| `attachments` | Attachment[] |  |
| `in_reply_to` | string | Message UUID or RFC Message-ID to reply to |
| `reply_all` | boolean | With in_reply_to: copy the other recipients |
| `send_at` | string (date-time) | Send automatically at this time (future, within 365 days) |

Responses:

* `201` Created: Draft
* `400` Error envelope: Error
* `404` Error envelope: Error

### GET /v0/inboxes/{inbox}/drafts/{draft}

Get a draft (needs message_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `draft` | string (uuid) | in path, required |

Responses:

* `200` OK: Draft
* `404` Error envelope: Error

### PATCH /v0/inboxes/{inbox}/drafts/{draft}

Update a draft (needs message_send)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `draft` | string (uuid) | in path, required |

Body (UpdateDraft):

| Name | Type | Notes |
| --- | --- | --- |
| `to` | string (email)[] |  |
| `cc` | string (email)[] |  |
| `bcc` | string (email)[] |  |
| `subject` | string |  |
| `text` | string |  |
| `html` | string |  |
| `labels` | string[] |  |
| `attachments` | Attachment[] |  |
| `send_at` | string,null (date-time) | null cancels a scheduled send |

Responses:

* `200` OK: Draft
* `400` Error envelope: Error
* `404` Error envelope: Error

### DELETE /v0/inboxes/{inbox}/drafts/{draft}

Delete a draft (needs message_send)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `draft` | string (uuid) | in path, required |

Responses:

* `204` Deleted

### POST /v0/inboxes/{inbox}/drafts/{draft}/send

Send a draft now and delete it (needs message_send)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `draft` | string (uuid) | in path, required |
| `Idempotency-Key` | string | in header, max length 256 |

Responses:

* `200` Accepted by the transport, or queued when the install has no transport: SendResult
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error
* `409` Error envelope: Error
* `429` Error envelope: Error
* `502` email_delivery_failed: the transport did not accept the message, with delivery_error_code and delivery_error: Error

### GET /v0/events/stream

Live events as Server Sent Events (needs message_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `types` | string | in query, Comma separated event types; all when absent |
| `inbox` | string (email) | in query, Only events of this inbox |
| `Last-Event-ID` | integer | in header, Resume after this event id, min 0 |
| `last_event_id` | integer | in query, Same as Last-Event-ID, for clients that cannot set headers, min 0 |

Responses:

* `200` Event stream: string
* `400` Error envelope: Error
* `404` Error envelope: Error
* `429` Error envelope: Error

### GET /v0/domains

List own and shared domains

Responses:

* `200` OK

### POST /v0/domains

Add a custom domain; returns the DNS records to publish

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `domain` | string | required |

Responses:

* `201` Created: Domain
* `403` Error envelope: Error
* `409` Error envelope: Error

### GET /v0/domains/{domain}

Get a domain with record status

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `domain` | string | in path, required |

Responses:

* `200` OK: Domain

### PATCH /v0/domains/{domain}

Move a domain into a group (or out of one), and nominate its catch-all inbox

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `domain` | string | in path, required |

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `group` | string (uuid) | The group id, or null to leave the group |
| `catch_all_inbox` | string (email) | The inbox that receives unknown local parts of this domain, or null to turn the catch-all off |

Responses:

* `200` OK: Domain
* `403` Error envelope: Error
* `404` Error envelope: Error
* `422` Error envelope: Error

### DELETE /v0/domains/{domain}

Delete a domain without inboxes

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `domain` | string | in path, required |

Responses:

* `204` Deleted
* `409` Error envelope: Error

### POST /v0/domains/{domain}/verify

Check DNS now (the scheduler also checks every 5 minutes for 7 days)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `domain` | string | in path, required |

Responses:

* `200` OK: Domain

### GET /v0/api-keys

List active keys (requires api_key_read)

Responses:

* `200` OK

### POST /v0/api-keys

Create a key with at most the caller's permissions (key shown once)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | max length 100 |
| `permissions` | Permission[] | omit to copy the caller's permissions |
| `expires_at` | string (date-time) |  |
| `scope` | KeyScopeInput |  |

Responses:

* `201` Created: ApiKey
* `403` Error envelope: Error

### DELETE /v0/api-keys/{key}

Revoke a key immediately (requires api_key_write, no escalation)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `key` | string (uuid) | in path, required |

Responses:

* `204` Revoked

### GET /v0/audit-logs

Security relevant actions of the organization (requires audit_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `limit` | integer | in query, min 1, max 200, default 50 |
| `actor_type` | user \| agent \| operator \| system | in query |
| `actor_key_id` | string (uuid) | in query |

Responses:

* `200` OK: object
* `400` Error envelope: Error
* `403` Error envelope: Error

### GET /v0/webhooks

List webhooks

Responses:

* `200` OK

### POST /v0/webhooks

Create a webhook (secret is returned once)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `url` | string (uri) | required |
| `event_types` | EventType[] | required, at least 1 item |
| `inbox_ids` | string (email)[] | Inboxes the caller can see (404 otherwise). A webhook keeps the scope of the caller who created it: events of inboxes outside that scope are never delivered, whatever inbox_ids says, and a scoped webhook receives events without an inbox only when its creator is an owner or admin., at most 10 items |

Responses:

* `201` Created: Webhook
* `404` Error envelope: Error

### DELETE /v0/webhooks/{webhook}

Delete a webhook

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `webhook` | string (uuid) | in path, required |

Responses:

* `204` Deleted

### GET /v0/billing

Plan and Stripe subscription state (requires billing_read)

Responses:

* `200` OK: Billing
* `403` Error envelope: Error
* `501` Error envelope: Error

### POST /v0/billing/checkout

Start a Stripe Checkout for a paid plan (requires billing_write, audited)

Body (Checkout):

| Name | Type | Notes |
| --- | --- | --- |
| `plan` | builder \| growth \| team | required |
| `interval` | monthly \| yearly | default monthly |
| `success_url` | string (uri) | Defaults to the deployment's MAI_BILLING_RETURN_URL |
| `cancel_url` | string (uri) | Defaults to the deployment's MAI_BILLING_RETURN_URL |

Responses:

* `200` OK: BillingSession
* `400` Error envelope: Error
* `403` Error envelope: Error
* `409` Error envelope: Error
* `501` Error envelope: Error
* `502` Error envelope: Error

### POST /v0/billing/portal

Open the Stripe customer portal (requires billing_write, audited)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `return_url` | string (uri) |  |

Responses:

* `200` OK: BillingSession
* `403` Error envelope: Error
* `409` Error envelope: Error
* `501` Error envelope: Error
* `502` Error envelope: Error

### GET /v0/export

Export the organization's own data (requires data_export, audited)

Responses:

* `200` OK: OrgExport
* `403` Error envelope: Error

### POST /v0/organizations

Create the organization of a signed in person (self service sign up)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | required, The organization name, trimmed, max length 80 |

Responses:

* `201` Created: NewOrganization
* `400` Error envelope: Error
* `401` Error envelope: Error
* `403` Error envelope: Error
* `409` Error envelope: Error
* `429` Error envelope: Error

### GET /v0/organization

The organization and the state of its outbound mail

Responses:

* `200` OK: Organization
* `401` Error envelope: Error
* `403` Error envelope: Error

### PATCH /v0/organization

Change the second factor policy or the retention windows (session or key holding member_write)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `require_mfa` | boolean |  |
| `retention` | RetentionPolicy |  |

Responses:

* `200` OK: object
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error
* `422` Error envelope: Error

### DELETE /v0/organization

Delete the organization and all its data (requires data_delete, audited)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `confirm` | string | required, The organization name, verbatim |

Responses:

* `200` Deleted; body carries the export taken before deletion: object
* `400` Error envelope: Error
* `403` Error envelope: Error

### GET /v0/tickets

List tickets, newest activity first

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `limit` | integer | in query, min 1, max 100, default 20 |
| `page_token` | string | in query |
| `state` | TicketState | in query |
| `priority` | TicketPriority | in query |
| `assignee` | string | in query, user id, API key id, or none |
| `inbox` | string | in query, inbox address |
| `q` | string | in query, case insensitive over subject and requester |

Responses:

* `200` OK: TicketList

### POST /v0/tickets

Open a ticket on an existing thread (requires ticket_write, audited)

Body (CreateTicket):

| Name | Type | Notes |
| --- | --- | --- |
| `thread_id` | string | required, The conversation to open the ticket on. A thread whose previous ticket is resolved or closed accepts a new ticket, with a new number. |
| `priority` | TicketPriority |  |
| `tags` | string[] |  |
| `assignee_user` | string |  |
| `assignee_key` | string |  |

Responses:

* `201` Created: Ticket
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error
* `409` Error envelope: Error

### GET /v0/tickets/stats

Queue counters and median first reply

Responses:

* `200` OK: TicketStats

### GET /v0/tickets/{ticket}

Read one ticket with its timeline and internal notes

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `ticket` | string | in path, required |

Responses:

* `200` OK: TicketDetail
* `404` Error envelope: Error

### PATCH /v0/tickets/{ticket}

Change state, priority, tags or assignment (requires ticket_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `ticket` | string | in path, required |

Body (UpdateTicket):

| Name | Type | Notes |
| --- | --- | --- |
| `state` | TicketState |  |
| `priority` | TicketPriority |  |
| `tags` | string[] |  |
| `assignee_user` | string |  |
| `assignee_key` | string |  |

Responses:

* `200` OK: Ticket
* `400` Error envelope: Error
* `404` Error envelope: Error
* `409` Error envelope: Error

### POST /v0/tickets/{ticket}/notes

Add an internal note (requires ticket_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `ticket` | string | in path, required |

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `body` | string | required |

Responses:

* `201` Created: TicketNote
* `400` Error envelope: Error
* `404` Error envelope: Error

### POST /v0/tickets/{ticket}/reply

Reply to the customer on the ticket thread (requires message_send, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `ticket` | string | in path, required |

Body (Reply):

| Name | Type | Notes |
| --- | --- | --- |
| `text` | string |  |
| `html` | string |  |
| `reply_all` | boolean |  |
| `labels` | string[] |  |
| `attachments` | Attachment[] |  |

Responses:

* `200` Sent: SendResult
* `403` Error envelope: Error
* `404` Error envelope: Error
* `409` Error envelope: Error

### POST /v0/tickets/{ticket}/linear

Create the Linear issue of a ticket (requires ticket_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `ticket` | string | in path, required |

Responses:

* `201` Created: TicketLink
* `404` Error envelope: Error
* `409` Error envelope: Error
* `501` Error envelope: Error
* `502` Error envelope: Error

### GET /v0/connectors

Which trackers this organization can send tickets to (requires ticket_read)

Responses:

* `200` OK: ConnectorList
* `403` Error envelope: Error

### POST /v0/connectors/{provider}/start

Begin the consent with a tracker (requires webhook_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `provider` | linear | in path, required |

Responses:

* `200` OK: ConnectorStart
* `404` Error envelope: Error
* `501` Error envelope: Error

### GET /v0/connectors/{provider}/callback

Where the tracker sends the browser after consent

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `provider` | linear | in path, required |
| `code` | string | in query |
| `state` | string | in query |

Responses:

* `303` Back to the console

### GET /v0/connectors/{provider}/teams

The teams of the connected workspace, read live (requires webhook_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `provider` | linear | in path, required |

Responses:

* `200` OK: ConnectorTeamList
* `409` Error envelope: Error
* `502` Error envelope: Error

### PATCH /v0/connectors/{provider}

Choose the team and switch automatic sending (requires webhook_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `provider` | linear | in path, required |

Body (UpdateConnector):

| Name | Type | Notes |
| --- | --- | --- |
| `team_id` | string |  |
| `auto_send` | boolean |  |

Responses:

* `200` OK: Connector
* `400` Error envelope: Error
* `409` Error envelope: Error

### DELETE /v0/connectors/{provider}

Disconnect a tracker (requires webhook_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `provider` | linear | in path, required |

Responses:

* `204` Disconnected
* `404` Error envelope: Error

### GET /v0/ticket-views

List the saved views of the queue (requires ticket_read)

Responses:

* `200` OK: TicketViewList
* `403` Error envelope: Error

### POST /v0/ticket-views

Save the current queue filter under a name (requires ticket_write, audited)

Body (CreateTicketView):

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | required, Up to 60 characters, unique among the views the caller can see |
| `shared` | boolean | Save it for the whole organization; an owner or an admin only |
| `filters` | TicketViewFilters | required |

Responses:

* `201` Created: TicketView
* `400` Error envelope: Error
* `403` Error envelope: Error
* `409` Error envelope: Error

### DELETE /v0/ticket-views/{view}

Delete a saved view (requires ticket_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `view` | string | in path, required |

Responses:

* `204` Deleted
* `403` Error envelope: Error
* `404` Error envelope: Error

### GET /v0/ticket-sla

Read the SLA policy of the organization (requires ticket_read)

Responses:

* `200` OK: TicketSLA
* `403` Error envelope: Error

### PATCH /v0/ticket-sla

Change the SLA policy (owner or admin, audited)

Body (UpdateTicketSLA):

| Name | Type | Notes |
| --- | --- | --- |
| `windows` | TicketSLAWindow[] |  |
| `targets` | TicketSLATargets |  |
| `auto_close_days` | integer | min 0, max 365 |

Responses:

* `200` OK: TicketSLA
* `400` Error envelope: Error
* `403` Error envelope: Error

### GET /v0/push/subscriptions

List the devices the caller registered (requires notification_write)

Responses:

* `200` OK: PushSubscriptionList
* `403` Error envelope: Error
* `501` Error envelope: Error

### POST /v0/push/subscriptions

Register a browser for web push (requires notification_write, audited)

Body (PushSubscribe):

| Name | Type | Notes |
| --- | --- | --- |
| `endpoint` | string (uri) | required, The device at its push service. A capability: never logged, never returned |
| `keys` | object | required |
| `expiration_time` | integer | Milliseconds since the epoch, or null |
| `user_agent` | string | The browser that subscribed, for the device label, max length 512 |

Responses:

* `201` Created (or updated): PushSubscription
* `400` Error envelope: Error
* `403` Error envelope: Error
* `409` Error envelope: Error
* `422` Error envelope: Error
* `429` Error envelope: Error
* `501` Error envelope: Error

### DELETE /v0/push/subscriptions

Drop a device by endpoint (requires notification_write, audited)

Body (PushUnsubscribe):

| Name | Type | Notes |
| --- | --- | --- |
| `endpoint` | string (uri) | required, The endpoint the browser is dropping |

Responses:

* `204` No content, the device is off
* `400` Error envelope: Error
* `403` Error envelope: Error
* `501` Error envelope: Error

### DELETE /v0/push/subscriptions/{subscription}

Drop a device by id (requires notification_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `subscription` | string (uuid) | in path, required |

Responses:

* `204` No content, the device is off
* `403` Error envelope: Error
* `404` Error envelope: Error
* `501` Error envelope: Error

### GET /v0/members

List the team (requires member_read)

Responses:

* `200` OK: MemberList
* `403` Error envelope: Error

### POST /v0/members

Invite somebody by email (requires member_write, audited)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `email` | string (email) | required |
| `role` | admin \| member | default member |

Responses:

* `201` Created; the invitation with its one time token: CreatedInvitation
* `400` Error envelope: Error
* `403` Error envelope: Error
* `409` Error envelope: Error
* `429` Error envelope: Error

### PATCH /v0/members/{user}

Change a role (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `user` | string (uuid) | in path, required |

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `role` | admin \| member | required |

Responses:

* `200` OK: Member
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error
* `409` Error envelope: Error

### DELETE /v0/members/{user}

Remove somebody (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `user` | string (uuid) | in path, required |

Responses:

* `204` No content
* `403` Error envelope: Error
* `404` Error envelope: Error

### GET /v0/members/invitations

List pending invitations (requires member_read)

Responses:

* `200` OK: InvitationList
* `403` Error envelope: Error

### POST /v0/members/invitations

Invite somebody by email (alias of POST /v0/members)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `email` | string (email) | required |
| `role` | admin \| member | default member |

Responses:

* `201` Created: CreatedInvitation
* `400` Error envelope: Error
* `403` Error envelope: Error
* `429` Error envelope: Error

### DELETE /v0/members/invitations/{invitation}

Cancel an invitation (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `invitation` | string (uuid) | in path, required |

Responses:

* `204` No content
* `403` Error envelope: Error
* `404` Error envelope: Error

### GET /v0/invitations/{token}

Preview an invitation (public)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `token` | string | in path, required |

Responses:

* `200` OK: InvitationPreview
* `404` Error envelope: Error
* `410` Error envelope: Error

### POST /v0/invitations/{token}/accept

Accept an invitation (public)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `token` | string | in path, required |

Responses:

* `200` Accepted; the session and the role: object
* `404` Error envelope: Error
* `409` Error envelope: Error
* `410` Error envelope: Error

### GET /v0/groups

List groups (requires group_read)

Responses:

* `200` OK: GroupList
* `403` Error envelope: Error

### POST /v0/groups

Create a group (requires group_write, audited)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | required, max length 60 |
| `color` | string | One of the eight palette tokens; a free form colour is refused |

Responses:

* `201` Created: Group
* `400` Error envelope: Error
* `403` Error envelope: Error
* `409` Error envelope: Error

### PATCH /v0/groups/{group}

Rename or recolour a group (requires group_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `group` | string (uuid) | in path, required |

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | max length 60 |
| `color` | string |  |

Responses:

* `200` OK: Group
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error

### DELETE /v0/groups/{group}

Delete a group (requires group_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `group` | string (uuid) | in path, required |

Responses:

* `204` No content
* `403` Error envelope: Error
* `404` Error envelope: Error

### GET /v0/inboxes/{inbox}/grants

Who can see this inbox (requires member_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |

Responses:

* `200` OK: GrantList
* `403` Error envelope: Error
* `404` Error envelope: Error

### PUT /v0/inboxes/{inbox}/grants

Grant somebody access to this inbox (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `user_id` | string (uuid) | required |
| `rights` | viewer \| responder | required |

Responses:

* `200` OK: Grant
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error

### DELETE /v0/inboxes/{inbox}/grants/{user}

Revoke access to this inbox (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `user` | string (uuid) | in path, required |

Responses:

* `204` No content
* `403` Error envelope: Error
* `404` Error envelope: Error

### GET /v0/domains/{domain}/grants

Who can see this domain (requires member_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `domain` | string | in path, required |

Responses:

* `200` OK: GrantList
* `403` Error envelope: Error
* `404` Error envelope: Error

### PUT /v0/domains/{domain}/grants

Grant somebody access to this domain (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `domain` | string | in path, required |

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `user_id` | string (uuid) | required |
| `rights` | viewer \| responder | required |

Responses:

* `200` OK: Grant
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error

### DELETE /v0/domains/{domain}/grants/{user}

Revoke access to this domain (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `domain` | string | in path, required |
| `user` | string (uuid) | in path, required |

Responses:

* `204` No content
* `403` Error envelope: Error
* `404` Error envelope: Error

### POST /stripe/webhook

Stripe webhook endpoint (public, Stripe-Signature verified)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `Stripe-Signature` | string | in header, required |

Responses:

* `200` Received; outcome is applied, duplicate, stale or ignored
* `400` Error envelope: Error
* `501` Error envelope: Error

### GET /v0/webhooks/{webhook}/events

Events delivered to this webhook, newest first, each with its last attempt (requires webhook_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `webhook` | string (uuid) | in path, required |
| `status` | pending \| succeeded \| failed | in query, Filter on the delivery state |
| `limit` | integer | in query, min 1, max 100, default 20 |
| `page_token` | string | in query |

Responses:

* `200` OK: WebhookEventList
* `403` Error envelope: Error
* `404` Error envelope: Error

### GET /v0/webhooks/{webhook}/events/{event}/attempts

Every attempt of one event, oldest first (requires webhook_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `webhook` | string (uuid) | in path, required |
| `event` | string (uuid) | in path, required |

Responses:

* `200` OK: WebhookAttemptList
* `403` Error envelope: Error
* `404` Error envelope: Error

### POST /v0/webhooks/{webhook}/events/{event}/replay

Queue the event again for this webhook, also when it succeeded (requires webhook_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `webhook` | string (uuid) | in path, required |
| `event` | string (uuid) | in path, required |

Responses:

* `202` Queued: WebhookReplay
* `403` Error envelope: Error
* `404` Error envelope: Error

### POST /v0/webhooks/{webhook}/rotate-secret

Issue a new signing secret; the previous one verifies for 24 more hours (requires webhook_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `webhook` | string (uuid) | in path, required |

Responses:

* `200` OK: RotateSecret
* `403` Error envelope: Error
* `404` Error envelope: Error

### GET /v0/auth/mfa

This person's second factor and the policy that applies to it

Responses:

* `200` OK: MFAStatus
* `403` Error envelope: Error

### POST /v0/auth/mfa/enroll

Start a second factor for the caller (secret, otpauth URL, ten recovery codes)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `challenge_token` | string |  |

Responses:

* `201` Created: MFAEnrollment
* `400` Error envelope: Error
* `403` Error envelope: Error
* `409` Error envelope: Error

### POST /v0/auth/mfa/verify

Confirm a pending second factor with a code from the authenticator

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `code` | string | required, max length 6 |
| `challenge_token` | string |  |

Responses:

* `200` OK: MFAVerify
* `400` Error envelope: Error
* `401` Error envelope: Error
* `403` Error envelope: Error
* `409` Error envelope: Error

### POST /v0/auth/mfa/disable

Remove the caller's second factor (needs a code)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `code` | string |  |
| `recovery_code` | string |  |

Responses:

* `204` No content
* `400` Error envelope: Error
* `401` Error envelope: Error
* `403` Error envelope: Error
* `409` Error envelope: Error

### POST /v0/auth/mfa/challenge

Second step of a sign in, with a code or a recovery code (public)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `challenge_token` | string | required |
| `code` | string |  |
| `recovery_code` | string |  |

Responses:

* `200` Session: Session
* `303` A browser goes to the login page with the session in the fragment
* `400` Error envelope: Error
* `401` Error envelope: Error
* `409` Error envelope: Error
* `429` Error envelope: Error

### GET /v0/inboxes/{inbox}/messages/search

Search the messages of an inbox, full text (requires message_read or message_metadata)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `q` | string | in query, Free text with quoted exact phrases, -exclusions and field:value tokens |
| `from` | string[] | in query, Sender: case and accent insensitive substring of the From header, repeatable, ORed |
| `to` | string[] | in query, Recipient (To or Cc): repeatable, ORed |
| `subject` | string[] | in query, Subject substring, repeatable, ORed |
| `has_attachment` | boolean | in query, Only messages that carry an attachment, or only those that do not |
| `label` | string[] | in query, Label, repeatable, ORed (any of them) |
| `is` | unread \| read \| flagged | in query, read, unread or flagged |
| `after` | string (date-time) | in query, Inclusive lower bound on created_at |
| `before` | string (date-time) | in query, Inclusive upper bound on created_at |
| `limit` | integer | in query, min 1, max 100, default 20 |
| `cursor` | string | in query, next_cursor of the previous page |
| `include_review` | boolean | in query, Also return messages held for security review; needs message_review, default false |

Responses:

* `200` OK: SearchResult
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error

### POST /v0/inboxes/{inbox}/messages/{message}/forward

Forward a message with its original attachments and an optional comment

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `message` | string | in path, required, Message UUID or RFC Message-ID (URL encoded) |
| `Idempotency-Key` | string | in header, max length 256 |

Body (Forward):

| Name | Type | Notes |
| --- | --- | --- |
| `to` | string (email)[] | required, at least 1 item |
| `cc` | string (email)[] |  |
| `bcc` | string (email)[] |  |
| `comment` | string | Optional text above the quoted message, below the signature |
| `labels` | string[] |  |

Responses:

* `200` Accepted by the transport, held by the undo window, or queued when the install has no transport: SendResult
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error
* `429` Error envelope: Error
* `502` email_delivery_failed: the transport refused the forward, with delivery_error_code and delivery_error: Error

### POST /v0/inboxes/{inbox}/messages/{message}/cancel

Cancel a held send inside its undo window (needs message_send)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `message` | string | in path, required, Message UUID or RFC Message-ID (URL encoded) |

Responses:

* `200` Cancelled: Message
* `403` Error envelope: Error
* `404` Error envelope: Error
* `409` Error envelope: Error

### POST /v0/messages/batch

Apply one action to up to 1.000 messages, with one outcome per id (requires message_update, or data_delete for a delete)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `ids` | string[] | required, Message UUIDs or RFC Message-IDs, at most 1.000; a repeated id is answered once per occurrence and applied once, at least 1 item, at most 1000 items |
| `action` | add_labels \| remove_labels \| archive \| mark_read \| mark_unread \| delete | required |
| `labels` | string[] | Required by add_labels and remove_labels, refused by the other four |

Responses:

* `200` One outcome per id, whatever they were: BatchResult
* `400` Error envelope: Error
* `403` Error envelope: Error
* `422` Error envelope: Error

### GET /v0/rules

List the rules of the organization (requires rule_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `target` | inbox \| domain \| org | in query, Only rules of this kind |
| `target_id` | string (uuid) | in query, Only rules of this inbox or domain |
| `enabled` | boolean | in query |

Responses:

* `200` OK: object
* `403` Error envelope: Error
* `501` Error envelope: Error

### POST /v0/rules

Create a rule (requires rule_write, audited)

Body (RuleInput):

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | required, max length 100 |
| `target` | RuleTarget |  |
| `priority` | integer | min 0, max 100000, default 100 |
| `enabled` | boolean | default true |
| `include_spam` | boolean | default false |
| `conditions` | RuleConditions | required |
| `actions` | RuleAction[] | required, at least 1 item, at most 20 items |

Responses:

* `201` Created: Rule
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error
* `422` Error envelope: Error
* `429` Error envelope: Error
* `501` Error envelope: Error

### GET /v0/rules/{rule}

Read one rule (requires rule_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `rule` | string (uuid) | in path, required |

Responses:

* `200` OK: Rule
* `403` Error envelope: Error
* `404` Error envelope: Error
* `501` Error envelope: Error

### PATCH /v0/rules/{rule}

Change a rule, or switch it off (requires rule_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `rule` | string (uuid) | in path, required |

Body (RulePatch):

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | max length 100 |
| `target` | RuleTarget |  |
| `priority` | integer | min 0, max 100000 |
| `enabled` | boolean |  |
| `include_spam` | boolean |  |
| `conditions` | RuleConditions |  |
| `actions` | RuleAction[] | at least 1 item, at most 20 items |

Responses:

* `200` OK: Rule
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error
* `422` Error envelope: Error
* `501` Error envelope: Error

### DELETE /v0/rules/{rule}

Delete a rule and any apply job queued for it (requires rule_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `rule` | string (uuid) | in path, required |

Responses:

* `204` Deleted
* `403` Error envelope: Error
* `404` Error envelope: Error
* `501` Error envelope: Error

### GET /v0/inboxes/{inbox}/agent

The inbox agent of one inbox, its actions and its state (requires inbox_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |

Responses:

* `200` OK: AgentConfig
* `403` Error envelope: Error
* `404` Error envelope: Error
* `501` Error envelope: Error

### PATCH /v0/inboxes/{inbox}/agent

Configure the inbox agent, replacing the action set and patching the rest (requires inbox_write)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |

Body (AgentConfigRequest):

| Name | Type | Notes |
| --- | --- | --- |
| `instructions` | string | 1 to 4000 characters, or null to clear, max length 4000 |
| `allowed_actions` | draft_reply \| draft_forward \| add_labels \| assign_thread \| mark_spam \| archive_thread \| move_to_trash[] | The whole set; an empty array turns the agent off, one of draft_reply, draft_forward, add_labels, assign_thread, mark_spam, archive_thread, move_to_trash |
| `tone` | concise_professional \| concise_casual \| expanded_professional \| expanded_casual |  |
| `enabled` | boolean |  |

Responses:

* `200` OK: AgentConfig
* `403` Error envelope: Error
* `404` Error envelope: Error
* `422` Error envelope: Error
* `501` Error envelope: Error

### POST /v0/rules/{rule}/apply

Apply a rule to the messages that already exist (requires rule_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `rule` | string (uuid) | in path, required |

Responses:

* `202` Queued: RuleApplyReport
* `403` Error envelope: Error
* `404` Error envelope: Error
* `409` Error envelope: Error
* `501` Error envelope: Error

### GET /v0/rules/applies/{apply}

The state of one apply job (requires rule_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `apply` | string (uuid) | in path, required |

Responses:

* `200` OK: RuleApplyReport
* `403` Error envelope: Error
* `404` Error envelope: Error
* `501` Error envelope: Error

### GET /v0/inboxes/{inbox}/auto-reply

The automatic answer of one inbox and whether it is inside its window (requires rule_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |

Responses:

* `200` OK: AutoReply
* `403` Error envelope: Error
* `404` Error envelope: Error

### PUT /v0/inboxes/{inbox}/auto-reply

Set the automatic answer of one inbox (requires rule_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |

Body (AutoReplyInput):

| Name | Type | Notes |
| --- | --- | --- |
| `mode` | fixed \| daily \| weekly \| monthly \| permanent | required |
| `timezone` | string | required, IANA zone, for example Europe/Rome; required, never derived from the server |
| `start` | string | RFC 3339 for fixed, HH:MM for daily, weekly and monthly; absent for permanent |
| `end` | string | RFC 3339 for fixed, HH:MM for daily, weekly and monthly; absent for permanent |
| `days` | string[] | Weekday tokens for weekly, day-of-month numbers for monthly, absent otherwise, at most 31 items |
| `subject` | string | required, max length 256 |
| `body_text` | string | required, max length 4000 |
| `body_html` | string |  |
| `restrict_to_domain` | boolean | default false |

Responses:

* `200` OK: AutoReply
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error
* `429` Error envelope: Error
* `501` Error envelope: Error

### DELETE /v0/inboxes/{inbox}/auto-reply

Remove the automatic answer of one inbox and the rule that carried it (requires rule_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |

Responses:

* `204` Removed
* `403` Error envelope: Error
* `404` Error envelope: Error

### GET /v0/inboxes/{inbox}/blocklist

The block and allow list of one inbox (requires inbox_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |

Responses:

* `200` OK: object
* `403` Error envelope: Error
* `404` Error envelope: Error

### POST /v0/inboxes/{inbox}/blocklist

Add an entry to the block or allow list of one inbox (requires inbox_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |

Body (BlocklistInput):

| Name | Type | Notes |
| --- | --- | --- |
| `list_kind` | block \| allow | default block |
| `kind` | sender \| domain | required |
| `value` | string | required, An address for kind sender, a domain for kind domain; never one as the other, max length 320 |

Responses:

* `201` Created: BlocklistEntry
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error
* `429` Error envelope: Error

### DELETE /v0/inboxes/{inbox}/blocklist

Remove an entry of the list of one inbox, named in the query string (requires inbox_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `kind` | sender \| domain | in query, required |
| `value` | string | in query, required |
| `list_kind` | block \| allow | in query, default block |

Responses:

* `204` Removed
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error

### GET /v0/organization/blocklist

The organization wide block and allow list (requires org_blocklist_write)

Responses:

* `200` OK: object
* `403` Error envelope: Error

### POST /v0/organization/blocklist

Add an entry to the organization wide list (requires org_blocklist_write, audited)

Body (BlocklistInput):

| Name | Type | Notes |
| --- | --- | --- |
| `list_kind` | block \| allow | default block |
| `kind` | sender \| domain | required |
| `value` | string | required, An address for kind sender, a domain for kind domain; never one as the other, max length 320 |

Responses:

* `201` Created: BlocklistEntry
* `400` Error envelope: Error
* `403` Error envelope: Error
* `429` Error envelope: Error

### DELETE /v0/organization/blocklist

Remove an entry of the organization wide list (requires org_blocklist_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `kind` | sender \| domain | in query, required |
| `value` | string | in query, required |
| `list_kind` | block \| allow | in query, default block |

Responses:

* `204` Removed
* `400` Error envelope: Error

### GET /v0/inboxes/{inbox}/messages/{message}/timeline

The delivery states of one message, oldest first, with the source of each (requires message_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) | in path, required |
| `message` | string | in path, required, Message UUID or RFC Message-ID (URL encoded) |

Responses:

* `200` OK: MessageTimeline
* `403` Error envelope: Error
* `404` Error envelope: Error

### GET /v0/domains/{domain}/reputation

Bounce and complaint reputation of a domain, measured like the sending breaker (requires domain_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `domain` | string | in path, required |

Responses:

* `200` OK: DomainReputation
* `403` Error envelope: Error
* `404` Error envelope: Error

### GET /v0/oauth/authorize

Authorize a third party app for the signed in person (session; creates the grant, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `client_id` | string | in query, required |
| `redirect_uri` | string (uri) | in query, required |
| `response_type` | code | in query, required |
| `scope` | string | in query |
| `state` | string | in query, max length 512 |
| `code_challenge` | string | in query, required |
| `code_challenge_method` | S256 | in query, required |
| `resource` | string | in query |

Responses:

* `200` OK: OAuthAuthorizeResult
* `400` Error envelope: Error
* `403` Error envelope: Error
* `501` Error envelope: Error

### POST /v0/oauth/token

Exchange an authorization code, or a refresh token, for an access token (public)

Body:

| Name | Type | Notes |
| --- | --- | --- |
| `grant_type` | authorization_code \| refresh_token | required |
| `code` | string |  |
| `redirect_uri` | string (uri) |  |
| `client_id` | string |  |
| `code_verifier` | string |  |
| `refresh_token` | string |  |

Responses:

* `200` OK: OAuthToken
* `400` invalid_request, invalid_grant, invalid_client, unsupported_grant_type (RFC 6749 body): object
* `501` oauth_disabled: object

### POST /v0/oauth/register

Register a client dynamically (public; public clients only, PKCE, rate limited per address)

Body (OAuthClientInput):

| Name | Type | Notes |
| --- | --- | --- |
| `client_name` | string | required, max length 100 |
| `client_uri` | string (uri) | Where a person can read about the app |
| `redirect_uris` | string (uri)[] | required, https, or http on a loopback host; no fragment, at least 1 item, at most 10 items |
| `scope` | messages:send \| full_access | The most this client may ever ask for, default messages:send |

Responses:

* `201` Created: OAuthClient
* `400` Error envelope: Error
* `429` Error envelope: Error
* `501` Error envelope: Error

### GET /v0/oauth/grants

The apps this person connected (session; a key is refused with session_required)

Responses:

* `200` OK: OAuthGrantList
* `403` Error envelope: Error
* `501` Error envelope: Error

### POST /v0/oauth/grants/{grant}/revoke

Revoke one grant, immediately on every token it issued (session; audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `grant` | string (uuid) | in path, required |

Responses:

* `204` No content
* `403` Error envelope: Error
* `404` Error envelope: Error
* `501` Error envelope: Error

### GET /v0/logs

Redacted API request log of the organization, newest first (requires audit_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `limit` | integer | in query, min 1, max 200, default 50 |
| `key_id` | string (uuid) | in query |
| `method` | string | in query |
| `status` | integer | in query, min 100, max 599 |
| `sdk` | go \| node \| python \| php \| ruby \| java | in query |
| `after` | string (date-time) | in query |
| `before` | string (date-time) | in query |

Responses:

* `200` OK: object
* `400` Error envelope: Error
* `403` Error envelope: Error
* `501` Error envelope: Error

### GET /v0/logs/{log}

One API request with its redacted request and response body (requires audit_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `log` | string (uuid) | in path, required |

Responses:

* `200` OK: RequestLog
* `403` Error envelope: Error
* `404` Error envelope: Error
* `501` Error envelope: Error

### GET /v0/logs/{log}/export

The same request as a markdown block a person can paste into an assistant (requires audit_read)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `log` | string (uuid) | in path, required |

Responses:

* `200` OK: string
* `403` Error envelope: Error
* `404` Error envelope: Error
* `501` Error envelope: Error

### GET /v0/suppressions

The suppression list of the organization (requires suppression_write)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `origin` | bounce \| complaint \| manual | in query |
| `limit` | integer | in query, min 1, max 100, default 20 |
| `page_token` | string | in query |

Responses:



### GET /v0/roles

List custom roles (requires member_read)

Responses:

* `200` OK: CustomRoleList
* `403` Error envelope: Error

### POST /v0/roles

Create a custom role (requires member_write, audited)

Body (CustomRoleRequest):

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | max length 60 |
| `description` | string | max length 200 |
| `permissions` | Permission[] | Required on create. An unknown name is 422 unknown_permission; a name the caller does not hold is 403 permission_escalation. |

Responses:

* `201` Created: CustomRole
* `400` Error envelope: Error
* `403` Error envelope: Error
* `409` Error envelope: Error
* `422` Error envelope: Error

### GET /v0/suppressions/export

The whole list as CSV (requires suppression_write, audited)

Responses:

* `200` OK: string
* `403` Error envelope: Error

### POST /v0/suppressions/import

Import a CSV of addresses (requires suppression_write, audited)

Responses:

* `201` Created: SuppressionResult
* `400` Error envelope: Error
* `403` Error envelope: Error
* `413` Error envelope: Error

### GET /v0/unsubscribe/{token}

The unsubscribe page of a one-click link (public)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `token` | string | in path, required |

Responses:

* `200` OK: string
* `404` The link is not valid: string
* `501` unsubscribe_disabled: this deployment has no signing secret or no public base URL

### POST /v0/unsubscribe/{token}

One-click unsubscribe (public, RFC 8058)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `token` | string | in path, required |

Responses:

* `200` OK: string
* `404` The link is not valid: string
* `501` unsubscribe_disabled: this deployment has no signing secret or no public base URL

### PATCH /v0/roles/{role}

Change a role's name, description or permission set (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `role` | string (uuid) | in path, required |

Body (CustomRoleRequest):

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | max length 60 |
| `description` | string | max length 200 |
| `permissions` | Permission[] | Required on create. An unknown name is 422 unknown_permission; a name the caller does not hold is 403 permission_escalation. |

Responses:

* `200` OK: CustomRole
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error
* `409` Error envelope: Error
* `422` Error envelope: Error

### DELETE /v0/roles/{role}

Delete a custom role (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `role` | string (uuid) | in path, required |

Responses:

* `204` Deleted, assignments revoked
* `403` Error envelope: Error
* `404` Error envelope: Error

### PUT /v0/members/{user}/roles

Replace the custom roles of a person (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `user` | string (uuid) | in path, required |

Body (RoleAssignmentRequest):

| Name | Type | Notes |
| --- | --- | --- |
| `role_ids` | string (uuid)[] | required, The whole set the subject should hold. An empty array revokes every custom role from the subject; that is a revocation, not a mistake., at most 20 items |

Responses:

* `200` OK: RoleAssignmentChange
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error

### PUT /v0/groups/{group}/roles

Replace the custom roles of a group (requires member_write, audited)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `group` | string (uuid) | in path, required |

Body (RoleAssignmentRequest):

| Name | Type | Notes |
| --- | --- | --- |
| `role_ids` | string (uuid)[] | required, The whole set the subject should hold. An empty array revokes every custom role from the subject; that is a revocation, not a mistake., at most 20 items |

Responses:

* `200` OK: RoleAssignmentChange
* `400` Error envelope: Error
* `403` Error envelope: Error
* `404` Error envelope: Error

### GET /v0/export/{export}

The state of one export job (requires data_export)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `export` | string (uuid) | in path, required |

Responses:

* `200` OK: ExportJob
* `403` Error envelope: Error
* `404` Error envelope: Error

### GET /v0/export/{export}/download

The document of a finished export job (requires data_export)

Parameters:

| Name | Type | Notes |
| --- | --- | --- |
| `export` | string (uuid) | in path, required |

Responses:

* `200` OK: OrgExport
* `403` Error envelope: Error
* `404` Error envelope: Error
* `409` Error envelope: Error

### GET /v0/organization/features

The feature switches of the caller's organization (read only)

Responses:

* `200` OK: object
* `401` Error envelope: Error

## Objects

### User object

| Name | Type | Notes |
| --- | --- | --- |
| `user_id` | string (uuid) |  |
| `email` | string (email) |  |
| `name` | string |  |
| `provider` | magic_link \| google \| microsoft \| github | Identity that first proved this address |
| `role` | owner \| admin \| member |  |
| `created_at` | string (date-time) |  |
| `last_login_at` | string (date-time) |  |

### Session object

| Name | Type | Notes |
| --- | --- | --- |
| `access_token` | string | Signed JWT, send as Authorization: Bearer, 15 minutes |
| `refresh_token` | string | Single use, 30 days, rotate it at POST /v0/auth/refresh |
| `token_type` | Bearer |  |
| `expires_in` | integer | Seconds until the access token expires |
| `expires_at` | string (date-time) |  |
| `refresh_expires_at` | string (date-time) |  |
| `role` | owner \| admin \| member |  |
| `user` | User |  |
| `organization` | Org |  |

### OAuthStart object

| Name | Type | Notes |
| --- | --- | --- |
| `provider` | google \| github |  |
| `authorize_url` | string | Provider URL, with state and the S256 PKCE challenge |
| `state` | string | Single use, 10 minutes |
| `expires_at` | string (date-time) |  |

### Me object

| Name | Type | Notes |
| --- | --- | --- |
| `auth` | api_key \| session |  |
| `organization_id` | string (uuid) | Empty when a session has no organization yet |
| `name` | string |  |
| `plan` | free \| builder \| growth \| team |  |
| `screen_before_notify` | boolean |  |
| `role` | owner \| admin \| member | Only for a session; null while it has no organization |
| `needs_organization` | boolean | True for a session that signed in and has not named an organization yet; only GET /v0/auth/me and POST /v0/organizations answer until then |
| `api_key_id` | string (uuid) | Only for an API key |
| `user` | object | Only for a session |
| `organization` | Org |  |
| `permissions` | string[] |  |
| `expires_at` | string (date-time) |  |
| `features` | OrganizationFeatures |  |

### Org object

| Name | Type | Notes |
| --- | --- | --- |
| `organization_id` | string (uuid) |  |
| `name` | string |  |
| `plan` | free \| builder \| growth \| team |  |
| `screen_before_notify` | boolean | Hold inbound mail until screened (operator setting: mai admin org-screening) |

### Organization object

| Name | Type | Notes |
| --- | --- | --- |
| `organization_id` | string (uuid) |  |
| `name` | string |  |
| `plan` | free \| builder \| growth \| team |  |
| `screen_before_notify` | boolean |  |
| `retention` | RetentionPolicy |  |
| `sending` | OrgSending |  |

### OrgSending object

| Name | Type | Notes |
| --- | --- | --- |
| `state` | ok \| degraded \| blocked | degraded: the last three sends in a row were refused by the transport. blocked: every attempt of the last 24 hours was refused for an authorization reason, so nothing can be delivered until the provider allows this server or the credentials are fixed. |
| `reason_code` | unauthorized_ip \| auth_failed \| relay_unreachable \| connection_refused \| tls_failed \| unknown | The fault code of the newest refused attempt; null while state is ok. |
| `since` | string (date-time) | When the current state began; null while state is ok. |

### NewOrganization object

| Name | Type | Notes |
| --- | --- | --- |
| `organization_id` | string (uuid) |  |
| `name` | string |  |
| `plan` | free \| builder \| growth \| team | Free: self service sign up needs no card |
| `role` | owner \| admin \| member | The caller's role in the new organization: owner |

### CreateInbox object

| Name | Type | Notes |
| --- | --- | --- |
| `username` | string | pattern ^[a-z0-9]([a-z0-9._-]{0,62}[a-z0-9])?$ |
| `domain` | string |  |
| `display_name` | string | max length 200 |
| `client_id` | string | pattern ^[A-Za-z0-9._~-]{1,256}$ |

### Inbox object

| Name | Type | Notes |
| --- | --- | --- |
| `id` | string (uuid) |  |
| `inbox_id` | string (email) |  |
| `email` | string (email) |  |
| `display_name` | string |  |
| `client_id` | string |  |
| `ticket_intake` | boolean | New threads on this inbox become tickets. |
| `sending_paused_at` | string (date-time) | Set when the breaker paused sending for this inbox; null when only the organization is paused, which sending_health reports in paused_scope. An operator resumes it. |
| `sending_paused_reason` | string |  |
| `sending_health` | SendingHealth |  |
| `signature_html` | string | The signature of this address, HTML, applied to every message composed after it was written. Changing it never rewrites a draft that already exists (the rule of Proton): a draft keeps the signature of its own moment. |
| `group` | GroupRef |  |
| `grants_count` | integer | How many people hold access directly on this inbox. Null unless the caller is the owner or an admin. |
| `created_at` | string (date-time) |  |
| `updated_at` | string (date-time) |  |

### SendingHealth object

| Name | Type | Notes |
| --- | --- | --- |
| `state` | ok \| paused | paused means sending from this inbox is refused with 403 sending_paused. |
| `paused_scope` | string | What paused sending: inbox, organization, or empty while state is ok. A send is refused by both scopes, so this is what tells them apart. Not an enum because the empty string is a value, and a validator that rejected it would fail on every healthy inbox. |
| `paused_at` | string (date-time) | When the breaker paused the inbox or its organization; null while state is ok. |
| `paused_reason` | string | The same sentence the 403 sending_paused carries, naming the window and the limit that was crossed, and the counts at the moment of the pause. |
| `window_hours` | integer | Length of the rolling window the counts cover. |
| `sends` | integer | Messages that reached a final outcome (accepted or rejected) in the window. |
| `hard_bounces` | integer | Permanent rejections at send time plus bounces reported later by a delivery status notification. |
| `complaints` | integer | Spam complaints reported through the feedback loop. |
| `bounce_rate` | number | hard_bounces divided by sends, 0 when sends is 0. |
| `complaint_rate` | number | complaints divided by sends, 0 when sends is 0. |
| `thresholds` | SendingThresholds |  |

### InboxList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `next_page_token` | string |  |
| `inboxes` | Inbox[] |  |

### Thread object

| Name | Type | Notes |
| --- | --- | --- |
| `thread_id` | string (uuid) |  |
| `inbox_id` | string |  |
| `subject` | string |  |
| `preview` | string |  |
| `labels` | string[] |  |
| `message_count` | integer |  |
| `timestamp` | string (date-time) |  |
| `messages` | Message[] |  |
| `retention_purged_at` | string (date-time) | Set when retention deleted the last message of the thread. A thread with no messages answers 200 with this and retention_notice, instead of 404, because a ticket may point at it. |
| `retention_notice` | string | The sentence in place of a body the retention window removed. |

### ThreadList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `next_page_token` | string |  |
| `threads` | Thread[] |  |

### Message object

| Name | Type | Notes |
| --- | --- | --- |
| `id` | string (uuid) |  |
| `message_id` | string | RFC 5322 Message-ID with angle brackets |
| `thread_id` | string (uuid) |  |
| `inbox_id` | string |  |
| `direction` | in \| out |  |
| `from` | string |  |
| `to` | string[] |  |
| `cc` | string[] |  |
| `subject` | string |  |
| `text` | string |  |
| `html` | string |  |
| `extracted_text` | string | New text without quoted history (set by the gatekeeper) |
| `preview` | string |  |
| `snippet` | string | Excerpt around the match. Only in search results, and empty for a key holding message_metadata without message_read |
| `labels` | string[] |  |
| `in_reply_to` | string |  |
| `references` | string[] |  |
| `status` | received \| queued \| sending \| sent \| failed \| held \| cancelled |  |
| `delivery_error_code` | unauthorized_ip \| auth_failed \| relay_unreachable \| connection_refused \| tls_failed \| unknown | Why an outbound message did not leave. Set when a delivery attempt was refused, absent otherwise; with status failed, the message is not delivered and the console says so. |
| `delivery_error` | string | What the mail server said when it refused the message, one line, at most 500 characters, never a credential. |
| `last_error` | string | The last reason a message did not leave, from the transport or from the platform: a send held by undo send whose window closed while sending was paused fails with the reason of the pause here, and no delivery_error, because no mail server refused it. |
| `cancel_window_ends_at` | string (date-time) | End of the undo window of a send born held. Before it, POST .../cancel withdraws the message; after it, the message is on its way and cancel answers 409 invalid_state. Absent on a message that never had a window. |
| `authentication_results` | object | What the SMTP edge verified for inbound mail: dkim, spf, dmarc, dmarc_policy, antivirus, spam_score, spam_action. Mail failing DMARC is labelled dmarc-fail, and spam under p=quarantine or p=reject. |
| `gatekeeper` | object |  |
| `screening` | review \| allowed \| released | Hold state under screen before notify; absent when the message was not held. Pending messages are never returned. |
| `attachments` | object[] |  |
| `created_at` | string (date-time) |  |

### MessageList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `next_page_token` | string |  |
| `messages` | Message[] |  |

### Attachment object

| Name | Type | Notes |
| --- | --- | --- |
| `filename` | string |  |
| `content_type` | string |  |
| `content` | string (base64) | required |

### Send object

| Name | Type | Notes |
| --- | --- | --- |
| `to` | string (email)[] | required, at least 1 item |
| `cc` | string (email)[] |  |
| `bcc` | string (email)[] |  |
| `subject` | string |  |
| `text` | string |  |
| `html` | string |  |
| `labels` | string[] |  |
| `attachments` | Attachment[] |  |

### Reply object

| Name | Type | Notes |
| --- | --- | --- |
| `text` | string |  |
| `html` | string |  |
| `reply_all` | boolean |  |
| `labels` | string[] |  |
| `attachments` | Attachment[] |  |

### SendResult object

| Name | Type | Notes |
| --- | --- | --- |
| `message_id` | string |  |
| `thread_id` | string (uuid) |  |
| `status` | queued \| held \| sent \| suppressed | State of the row the request created. held means the undo window is open: nothing has been attempted yet, and cancel may still withdraw it. suppressed means every recipient is on the suppression list: no message exists because there was nobody to write to. |
| `cancel_window_ends_at` | string (date-time) | Deadline of POST .../cancel, present when status is held. |
| `recipients` | RecipientOutcome[] | One entry per recipient of the request, in the order it named them. A suppressed recipient is reported here and not hidden: the message is never written for it. |

### CreateDraft object

| Name | Type | Notes |
| --- | --- | --- |
| `to` | string (email)[] |  |
| `cc` | string (email)[] |  |
| `bcc` | string (email)[] |  |
| `subject` | string |  |
| `text` | string |  |
| `html` | string |  |
| `labels` | string[] |  |
| `attachments` | Attachment[] |  |
| `in_reply_to` | string | Message UUID or RFC Message-ID to reply to |
| `reply_all` | boolean | With in_reply_to: copy the other recipients |
| `send_at` | string (date-time) | Send automatically at this time (future, within 365 days) |

### UpdateDraft object

| Name | Type | Notes |
| --- | --- | --- |
| `to` | string (email)[] |  |
| `cc` | string (email)[] |  |
| `bcc` | string (email)[] |  |
| `subject` | string |  |
| `text` | string |  |
| `html` | string |  |
| `labels` | string[] |  |
| `attachments` | Attachment[] |  |
| `send_at` | string,null (date-time) | null cancels a scheduled send |

### Draft object

| Name | Type | Notes |
| --- | --- | --- |
| `draft_id` | string (uuid) |  |
| `inbox_id` | string (email) |  |
| `thread_id` | string (uuid) | Set for a reply draft |
| `in_reply_to` | string | RFC Message-ID of the message replied to |
| `references` | string[] |  |
| `to` | string[] |  |
| `cc` | string[] |  |
| `bcc` | string[] |  |
| `subject` | string |  |
| `text` | string |  |
| `html` | string |  |
| `labels` | string[] |  |
| `attachments` | object[] |  |
| `send_at` | string (date-time) |  |
| `status` | draft \| scheduled \| failed |  |
| `last_error` | string | Why a scheduled send was refused |
| `created_at` | string (date-time) |  |
| `updated_at` | string (date-time) |  |

### DraftList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `next_page_token` | string |  |
| `drafts` | Draft[] |  |

### Webhook object

| Name | Type | Notes |
| --- | --- | --- |
| `webhook_id` | string (uuid) |  |
| `url` | string |  |
| `event_types` | EventType[] |  |
| `inbox_ids` | string[] |  |
| `enabled` | boolean |  |
| `secret` | string | whsec_..., returned only on create |

### Domain object

| Name | Type | Notes |
| --- | --- | --- |
| `domain_id` | string (uuid) |  |
| `domain` | string |  |
| `shared` | boolean |  |
| `status` | pending \| verified |  |
| `records` | object[] |  |
| `last_checked_at` | string (date-time) |  |
| `verified_at` | string (date-time) |  |
| `catch_all_inbox` | string (email) | The inbox that receives mail addressed to a local part nobody created on this domain, or null for no catch-all (D5) |

### PushSubscribe object

| Name | Type | Notes |
| --- | --- | --- |
| `endpoint` | string (uri) | required, The device at its push service. A capability: never logged, never returned |
| `keys` | object | required |
| `expiration_time` | integer | Milliseconds since the epoch, or null |
| `user_agent` | string | The browser that subscribed, for the device label, max length 512 |

### PushUnsubscribe object

| Name | Type | Notes |
| --- | --- | --- |
| `endpoint` | string (uri) | required, The endpoint the browser is dropping |

### PushSubscription object

| Name | Type | Notes |
| --- | --- | --- |
| `push_subscription_id` | string (uuid) |  |
| `device` | string | Label derived from the user agent, for example iPhone |
| `user_agent` | string |  |
| `created_at` | string (date-time) |  |
| `last_used_at` | string (date-time) |  |

### PushSubscriptionList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `push_subscriptions` | PushSubscription[] |  |

### Ticket object

| Name | Type | Notes |
| --- | --- | --- |
| `id` | string |  |
| `number` | integer |  |
| `inbox_id` | string |  |
| `thread_id` | string |  |
| `subject` | string |  |
| `requester` | string |  |
| `state` | TicketState |  |
| `priority` | TicketPriority |  |
| `assignee_user` | string |  |
| `assignee_key` | string |  |
| `sla_policy` | string |  |
| `due_first_reply` | string (date-time) |  |
| `due_resolution` | string (date-time) |  |
| `first_reply_at` | string (date-time) |  |
| `resolved_at` | string (date-time) |  |
| `closed_at` | string (date-time) |  |
| `tags` | string[] |  |
| `created_at` | string (date-time) |  |
| `updated_at` | string (date-time) |  |
| `body_available` | boolean | Whether the thread still has messages. False after retention emptied it; the ticket stays readable and retention_notice says why. Present on a single ticket read. |
| `retention_notice` | string | The sentence explaining that retention deleted the conversation. |

### TicketList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `next_page_token` | string |  |
| `tickets` | Ticket[] |  |

### TicketEvent object

| Name | Type | Notes |
| --- | --- | --- |
| `id` | integer |  |
| `kind` | string |  |
| `actor` | string |  |
| `detail` | object |  |
| `created_at` | string (date-time) |  |

### TicketNote object

| Name | Type | Notes |
| --- | --- | --- |
| `id` | string |  |
| `author` | string |  |
| `body` | string |  |
| `created_at` | string (date-time) |  |

### TicketDetail object

| Name | Type | Notes |
| --- | --- | --- |
| `ticket` | Ticket |  |
| `events` | TicketEvent[] |  |
| `notes` | TicketNote[] |  |
| `links` | TicketLink[] | Where the ticket was sent, one entry per tracker |

### TicketStats object

| Name | Type | Notes |
| --- | --- | --- |
| `by_state` | object |  |
| `open` | integer |  |
| `breaching` | integer |  |
| `unassigned` | integer |  |
| `median_first_reply_seconds` | integer |  |

### CreateTicket object

| Name | Type | Notes |
| --- | --- | --- |
| `thread_id` | string | required, The conversation to open the ticket on. A thread whose previous ticket is resolved or closed accepts a new ticket, with a new number. |
| `priority` | TicketPriority |  |
| `tags` | string[] |  |
| `assignee_user` | string |  |
| `assignee_key` | string |  |

### UpdateTicket object

| Name | Type | Notes |
| --- | --- | --- |
| `state` | TicketState |  |
| `priority` | TicketPriority |  |
| `tags` | string[] |  |
| `assignee_user` | string |  |
| `assignee_key` | string |  |

### TicketViewFilters object

| Name | Type | Notes |
| --- | --- | --- |
| `state` | TicketState |  |
| `priority` | TicketPriority |  |
| `assignee` | string | user id, API key id, or none for unassigned |
| `inbox` | string | inbox address |
| `q` | string | case insensitive over subject and requester |

### TicketView object

| Name | Type | Notes |
| --- | --- | --- |
| `view_id` | string |  |
| `name` | string |  |
| `shared` | boolean | true when the whole organization sees it |
| `owner_user_id` | string | The person the view belongs to; absent on a shared view |
| `filters` | TicketViewFilters |  |
| `created_at` | string (date-time) |  |

### TicketViewList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `ticket_views` | TicketView[] |  |

### CreateTicketView object

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | required, Up to 60 characters, unique among the views the caller can see |
| `shared` | boolean | Save it for the whole organization; an owner or an admin only |
| `filters` | TicketViewFilters | required |

### TicketSLAWindow object

| Name | Type | Notes |
| --- | --- | --- |
| `weekday` | integer | required, ISO weekday: 1 is Monday, 7 is Sunday, min 1, max 7 |
| `start` | string | required, HH:MM in the window timezone |
| `end` | string | required, HH:MM, after start; a window never crosses midnight |
| `timezone` | string | required, A real IANA zone, for example Europe/Berlin |

### TicketSLATargets object

| Name | Type | Notes |
| --- | --- | --- |
| `first_reply` | object |  |
| `resolution` | object |  |

### TicketSLA object

| Name | Type | Notes |
| --- | --- | --- |
| `windows` | TicketSLAWindow[] |  |
| `targets` | TicketSLATargets |  |
| `auto_close_days` | integer | Days a resolved ticket may stay silent before it closes; 0 disables |
| `source` | default \| organization | default means the plan table, untouched |
| `updated_by` | string |  |
| `updated_at` | string (date-time) |  |
| `note` | string | The sentence that says a change is not retroactive |

### UpdateTicketSLA object

| Name | Type | Notes |
| --- | --- | --- |
| `windows` | TicketSLAWindow[] |  |
| `targets` | TicketSLATargets |  |
| `auto_close_days` | integer | min 0, max 365 |

### TicketLink object

| Name | Type | Notes |
| --- | --- | --- |
| `provider` | linear |  |
| `key` | string | The identifier in the tracker, for example ENG-12 |
| `url` | string |  |
| `created_at` | string (date-time) |  |

### Connector object

| Name | Type | Notes |
| --- | --- | --- |
| `provider` | linear |  |
| `name` | string |  |
| `available` | boolean |  |
| `connected` | boolean |  |
| `needs_reconnect` | boolean |  |
| `workspace` | string |  |
| `team_id` | string |  |
| `team_name` | string |  |
| `auto_send` | boolean |  |
| `connected_at` | string (date-time) |  |

### ConnectorList object

| Name | Type | Notes |
| --- | --- | --- |
| `connectors` | Connector[] |  |

### ConnectorStart object

| Name | Type | Notes |
| --- | --- | --- |
| `provider` | linear |  |
| `authorize_url` | string |  |
| `expires_at` | string (date-time) |  |

### ConnectorTeam object

| Name | Type | Notes |
| --- | --- | --- |
| `id` | string |  |
| `key` | string |  |
| `name` | string |  |

### ConnectorTeamList object

| Name | Type | Notes |
| --- | --- | --- |
| `teams` | ConnectorTeam[] |  |

### UpdateConnector object

| Name | Type | Notes |
| --- | --- | --- |
| `team_id` | string |  |
| `auto_send` | boolean |  |

### Checkout object

| Name | Type | Notes |
| --- | --- | --- |
| `plan` | builder \| growth \| team | required |
| `interval` | monthly \| yearly | default monthly |
| `success_url` | string (uri) | Defaults to the deployment's MAI_BILLING_RETURN_URL |
| `cancel_url` | string (uri) | Defaults to the deployment's MAI_BILLING_RETURN_URL |

### BillingSession object

| Name | Type | Notes |
| --- | --- | --- |
| `session_id` | string |  |
| `url` | string (uri) |  |

### Billing object

| Name | Type | Notes |
| --- | --- | --- |
| `plan` | free \| builder \| growth \| team |  |
| `status` | none \| incomplete \| incomplete_expired \| trialing \| active \| past_due \| unpaid \| canceled \| paused |  |
| `interval` | monthly \| yearly |  |
| `current_period_end` | string (date-time) |  |
| `cancel_at_period_end` | boolean |  |
| `past_due_since` | string (date-time) |  |
| `grace_period_ends_at` | string (date-time) | Set while past_due or unpaid; the plan drops to free after it |

### Member object

| Name | Type | Notes |
| --- | --- | --- |
| `user_id` | string (uuid) |  |
| `email` | string (email) |  |
| `name` | string |  |
| `role` | owner \| admin \| member |  |
| `last_login_at` | string (date-time) |  |
| `created_at` | string (date-time) |  |
| `inbox_count` | integer | How many inboxes this person can see |
| `invited_at` | string (date-time) |  |
| `accepted_at` | string (date-time) |  |

### MemberList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `members` | Member[] |  |

### Invitation object

| Name | Type | Notes |
| --- | --- | --- |
| `invitation_id` | string (uuid) |  |
| `email` | string (email) |  |
| `role` | admin \| member |  |
| `invited_by` | string |  |
| `expires_at` | string (date-time) |  |
| `created_at` | string (date-time) |  |

### InvitationList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `invitations` | Invitation[] |  |

### InvitationPreview object

| Name | Type | Notes |
| --- | --- | --- |
| `organization` | string |  |
| `email` | string (email) |  |
| `role` | admin \| member |  |
| `expires_at` | string (date-time) |  |

### Group object

| Name | Type | Notes |
| --- | --- | --- |
| `group_id` | string (uuid) |  |
| `name` | string |  |
| `slug` | string |  |
| `color` | string |  |
| `inbox_count` | integer |  |
| `domain_count` | integer |  |
| `created_at` | string (date-time) |  |

### KeyScopeInput object

| Name | Type | Notes |
| --- | --- | --- |
| `kind` | org \| domain \| user | Defaults to org for the owner and an admin, to user for a member. A member may only ask for user. |
| `id` | string (uuid) | The domain, when kind is domain |

### ApiKey object

| Name | Type | Notes |
| --- | --- | --- |
| `api_key_id` | string (uuid) |  |
| `api_key` | string | The secret, returned once on create |
| `prefix` | string |  |
| `name` | string |  |
| `permissions` | Permission[] | Null means every permission the role allows |
| `scope` | object |  |
| `expires_at` | string (date-time) |  |
| `last_used_at` | string (date-time) |  |
| `created_at` | string (date-time) |  |
| `request_count` | integer | Requests this key has authenticated |
| `inactive_since` | string (date-time) | The last use, or the creation of a key never used |
| `stale` | boolean | True when the key has not been used for 30 days. The operator command mai admin keys-stale applies the same rule and can revoke such keys. |

### GroupRef object

| Name | Type | Notes |
| --- | --- | --- |
| `group_id` | string (uuid) |  |
| `name` | string |  |
| `color` | string |  |

### GroupList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `groups` | Group[] |  |

### Grant object

| Name | Type | Notes |
| --- | --- | --- |
| `user_id` | string (uuid) |  |
| `email` | string (email) |  |
| `name` | string |  |
| `rights` | viewer \| responder |  |
| `created_at` | string (date-time) |  |

### GrantList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `grants` | Grant[] |  |

### OrgExport object

| Name | Type | Notes |
| --- | --- | --- |
| `format` | string |  |
| `generated_at` | string (date-time) |  |
| `organization` | Org |  |
| `inboxes` | object[] |  |
| `domains` | object[] |  |
| `webhooks` | object[] |  |
| `api_keys` | object[] | Metadata only; the key and its hash are never exported. |
| `counts` | object |  |
| `audit_log` | object[] |  |
| `notice` | string | What this export deliberately leaves out, in plain language |

### SendingThresholds object

| Name | Type | Notes |
| --- | --- | --- |
| `max_bounce_rate` | number | Hard bounce rate above which sending pauses. |
| `max_complaint_rate` | number | Complaint rate above which sending pauses, once min_complaint_sends is reached. |
| `min_sends` | integer | Sends needed in the window before the bounce rate applies. |
| `min_complaint_sends` | integer | Sends needed before the complaint rate applies; below it the limit is a count. |
| `small_sender_complaints` | integer | Complaints in the window that pause sending below min_complaint_sends. |

### WebhookEvent object

| Name | Type | Notes |
| --- | --- | --- |
| `event_id` | string (uuid) |  |
| `event_type` | string |  |
| `status` | pending \| succeeded \| failed | Delivery state |
| `attempts` | integer | Attempts claimed so far |
| `last_attempt_status` | succeeded \| failed |  |
| `last_status_code` | integer |  |
| `last_latency_ms` | integer |  |
| `last_attempt_at` | string (date-time) |  |
| `created_at` | string (date-time) |  |

### WebhookEventList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `events` | WebhookEvent[] |  |
| `next_page_token` | string |  |

### WebhookAttempt object

| Name | Type | Notes |
| --- | --- | --- |
| `attempt_no` | integer |  |
| `status` | succeeded \| failed |  |
| `status_code` | integer | Null when the connection failed |
| `latency_ms` | integer |  |
| `response_excerpt` | string | At most 2 KB of the response body with credentials redacted; response headers are never stored, max length 2048 |
| `error` | string |  |
| `created_at` | string (date-time) |  |

### WebhookAttemptList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `attempts` | WebhookAttempt[] |  |

### WebhookReplay object

| Name | Type | Notes |
| --- | --- | --- |
| `event_id` | string (uuid) |  |
| `delivery_id` | integer |  |
| `status` | pending | It is queued; the attempt follows |
| `attempts` | integer | Attempts recorded before this replay |

### RotateSecret object

| Name | Type | Notes |
| --- | --- | --- |
| `webhook_id` | string (uuid) |  |
| `secret` | string | The new signing secret, shown once |
| `rotated_at` | string (date-time) |  |
| `previous_secret_valid_until` | string (date-time) | Until then the dispatcher signs with the previous secret too |

### MFAStatus object

| Name | Type | Notes |
| --- | --- | --- |
| `enrolled` | boolean | A confirmed factor; a pending secret does not count |
| `confirmed_at` | string (date-time) |  |
| `recovery_codes_left` | integer | min 0, max 10 |
| `required_by_organization` | boolean | The organization requires a factor; with enrolled false every other route answers 403 mfa_enrollment_required |
| `organization_policy` | MFAPolicy |  |

### MFAPolicy object

| Name | Type | Notes |
| --- | --- | --- |
| `require_mfa` | boolean |  |

### MFAEnrollment object

| Name | Type | Notes |
| --- | --- | --- |
| `secret` | string | Base32 TOTP secret, for manual entry |
| `otpauth_url` | string | otpauth:// URI a QR code is drawn from (SHA1, 6 digits, 30 seconds) |
| `recovery_codes` | string[] | Ten single use codes; only their hashes are stored, at least 10 item, at most 10 items |

### MFAVerify object

| Name | Type | Notes |
| --- | --- | --- |
| `enrolled` | boolean |  |
| `recovery_codes_left` | integer |  |
| `session` | Session |  |

### MFAStep object

| Name | Type | Notes |
| --- | --- | --- |
| `mfa_required` | true |  |
| `challenge_token` | string | Single use, five minutes, five attempts; redeem at POST /v0/auth/mfa/challenge |
| `kind` | verify \| enroll | verify when the factor exists, enroll when the organization requires one this person has not set up |
| `expires_at` | string (date-time) |  |

### SearchResult object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `next_cursor` | string | Pass as cursor= for the next page; absent on the last one |
| `messages` | Message[] |  |

### Forward object

| Name | Type | Notes |
| --- | --- | --- |
| `to` | string (email)[] | required, at least 1 item |
| `cc` | string (email)[] |  |
| `bcc` | string (email)[] |  |
| `comment` | string | Optional text above the quoted message, below the signature |
| `labels` | string[] |  |

### BatchResult object

| Name | Type | Notes |
| --- | --- | --- |
| `applied` | integer | Lines whose action happened (or had already happened: every action is idempotent) |
| `failed` | integer | Lines with a per item failure: not_found, forbidden or invalid |
| `results` | BatchItem[] |  |

### BatchItem object

| Name | Type | Notes |
| --- | --- | --- |
| `id` | string | The id as it was sent |
| `status` | applied \| not_found \| forbidden \| invalid |  |
| `error` | string | Why this item did not apply; absent on applied |

### RuleTarget object

| Name | Type | Notes |
| --- | --- | --- |
| `kind` | inbox \| domain \| org | required |
| `id` | string (uuid) | The inbox or the domain id; absent for kind org |

### RuleSize object

| Name | Type | Notes |
| --- | --- | --- |
| `larger` | integer (int64) | min 0 |
| `smaller` | integer (int64) | min 0 |

### RuleConditions object

| Name | Type | Notes |
| --- | --- | --- |
| `from` | string[] | Sender, case and accent insensitive substring, ORed, at most 32 items |
| `to` | string[] | Recipient (To or Cc), ORed, at most 32 items |
| `subject` | string[] | Subject substring, ORed, at most 32 items |
| `query` | string | Full text, same parser as search, max length 512 |
| `negated_query` | string | The message must not match this, max length 512 |
| `has_attachment` | boolean |  |
| `size` | RuleSize |  |
| `spam_verdict` | clean \| review \| spam[] | at most 3 items, one of clean, review, spam |

### RuleAction object

| Name | Type | Notes |
| --- | --- | --- |
| `type` | add_label \| remove_label \| archive \| move_to_folder \| mark_read \| forward_to \| auto_reply \| reject \| webhook \| assign | required |
| `label` | string | add_label, remove_label, move_to_folder: the label, which is also the folder, max length 64 |
| `address` | string (email) | forward_to: a verified address of the organization |
| `reason` | string | reject: one line, no address, sent back in the 550, max length 200 |
| `webhook_id` | string (uuid) | webhook: the endpoint that receives rule.matched |
| `subject` | string | auto_reply: the subject of the answer |
| `text` | string | auto_reply: the body of the answer, max length 4000 |
| `assignee` | string (uuid) | assign: the member who takes the ticket |

### Rule object

| Name | Type | Notes |
| --- | --- | --- |
| `rule_id` | string (uuid) |  |
| `name` | string |  |
| `target` | RuleTarget |  |
| `priority` | integer | Lower is evaluated first; ties go to the older rule |
| `enabled` | boolean |  |
| `include_spam` | boolean | False means the rule never sees a message with the spam verdict |
| `conditions` | RuleConditions |  |
| `actions` | RuleAction[] | at most 20 items |
| `created_at` | string (date-time) |  |
| `updated_at` | string (date-time) |  |

### RuleInput object

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | required, max length 100 |
| `target` | RuleTarget |  |
| `priority` | integer | min 0, max 100000, default 100 |
| `enabled` | boolean | default true |
| `include_spam` | boolean | default false |
| `conditions` | RuleConditions | required |
| `actions` | RuleAction[] | required, at least 1 item, at most 20 items |

### RulePatch object

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | max length 100 |
| `target` | RuleTarget |  |
| `priority` | integer | min 0, max 100000 |
| `enabled` | boolean |  |
| `include_spam` | boolean |  |
| `conditions` | RuleConditions |  |
| `actions` | RuleAction[] | at least 1 item, at most 20 items |

### RuleApplyReport object

| Name | Type | Notes |
| --- | --- | --- |
| `job_id` | string (uuid) |  |
| `rule_id` | string (uuid) |  |
| `status` | pending \| running \| done |  |
| `matched` | integer |  |
| `applied` | integer |  |
| `skipped` | object |  |
| `created_at` | string (date-time) |  |
| `finished_at` | string (date-time) |  |

### AgentAction object

| Name | Type | Notes |
| --- | --- | --- |
| `name` | draft_reply \| draft_forward \| add_labels \| assign_thread \| mark_spam \| archive_thread \| move_to_trash |  |
| `route` | string | The API operation a person uses to do the same thing |
| `summary` | string |  |

### AgentConfig object

| Name | Type | Notes |
| --- | --- | --- |
| `enabled` | boolean | The effective state: the switch on and at least one action. An empty action set is off. |
| `configured` | boolean | False when this inbox was never configured; the rest of the answer is then the default form |
| `disabled_reason` | not_configured \| no_actions \| switched_off | Absent when the agent is on |
| `instructions` | string | Empty when none are stored |
| `tone` | concise_professional \| concise_casual \| expanded_professional \| expanded_casual |  |
| `allowed_actions` | draft_reply \| draft_forward \| add_labels \| assign_thread \| mark_spam \| archive_thread \| move_to_trash[] | The whole set, in policy order; empty means the agent is off, one of draft_reply, draft_forward, add_labels, assign_thread, mark_spam, archive_thread, move_to_trash |
| `available_actions` | AgentAction[] | The vocabulary a configuration may name |
| `budget_per_hour` | integer | Actions this inbox may take in one hour |
| `actions_used` | integer | Actions the agent took in the current hour |
| `agent_key_id` | string (uuid) | The key that configured the agent: its identity in the audit log |
| `agent_key_name` | string |  |
| `updated_at` | string (date-time) |  |

### AgentConfigRequest object

| Name | Type | Notes |
| --- | --- | --- |
| `instructions` | string | 1 to 4000 characters, or null to clear, max length 4000 |
| `allowed_actions` | draft_reply \| draft_forward \| add_labels \| assign_thread \| mark_spam \| archive_thread \| move_to_trash[] | The whole set; an empty array turns the agent off, one of draft_reply, draft_forward, add_labels, assign_thread, mark_spam, archive_thread, move_to_trash |
| `tone` | concise_professional \| concise_casual \| expanded_professional \| expanded_casual |  |
| `enabled` | boolean |  |

### AutoReply object

| Name | Type | Notes |
| --- | --- | --- |
| `inbox` | string (email) |  |
| `rule_id` | string (uuid) | The one ordinary rule that carries the answer; the guards are its guards |
| `mode` | fixed \| daily \| weekly \| monthly \| permanent |  |
| `timezone` | string | The IANA zone the window is read in, always explicit |
| `start` | string | RFC 3339 instant for fixed, HH:MM clock time for daily, weekly and monthly |
| `end` | string | The other end of the window; an end before the start wraps past midnight |
| `days` | string[] | Weekday tokens (mon..sun) in weekly mode, day-of-month numbers in monthly mode, empty otherwise |
| `subject` | string |  |
| `body_text` | string |  |
| `body_html` | string |  |
| `restrict_to_domain` | boolean | Answer only senders on the inbox's own domain or on a domain the organization verified |
| `active` | boolean | Whether the window covers now, in the schedule's timezone |
| `updated_at` | string (date-time) |  |

### AutoReplyInput object

| Name | Type | Notes |
| --- | --- | --- |
| `mode` | fixed \| daily \| weekly \| monthly \| permanent | required |
| `timezone` | string | required, IANA zone, for example Europe/Rome; required, never derived from the server |
| `start` | string | RFC 3339 for fixed, HH:MM for daily, weekly and monthly; absent for permanent |
| `end` | string | RFC 3339 for fixed, HH:MM for daily, weekly and monthly; absent for permanent |
| `days` | string[] | Weekday tokens for weekly, day-of-month numbers for monthly, absent otherwise, at most 31 items |
| `subject` | string | required, max length 256 |
| `body_text` | string | required, max length 4000 |
| `body_html` | string |  |
| `restrict_to_domain` | boolean | default false |

### BlocklistEntry object

| Name | Type | Notes |
| --- | --- | --- |
| `entry_id` | string (uuid) |  |
| `scope` | inbox \| org |  |
| `scope_id` | string (uuid) | The inbox, absent for the organization list |
| `inbox` | string (email) |  |
| `list_kind` | block \| allow | allow beats block: an allowed address passes through a blocked domain |
| `kind` | sender \| domain |  |
| `value` | string | The lowered address or domain, matched exactly |
| `created_at` | string (date-time) |  |

### BlocklistInput object

| Name | Type | Notes |
| --- | --- | --- |
| `list_kind` | block \| allow | default block |
| `kind` | sender \| domain | required |
| `value` | string | required, An address for kind sender, a domain for kind domain; never one as the other, max length 320 |

### MessageTimeline object

| Name | Type | Notes |
| --- | --- | --- |
| `message_id` | string (uuid) |  |
| `inbox_id` | string | The address that holds the message, never the list of recipients |
| `status` | received \| queued \| sending \| sent \| failed \| held \| cancelled | The state of the row now; the events carry how it got there |
| `events` | MessageTimelineEvent[] |  |

### MessageTimelineEvent object

| Name | Type | Notes |
| --- | --- | --- |
| `type` | message.received \| message.received.spam \| message.sent \| message.delivered \| message.delivery_delayed \| message.failed \| message.rejected \| message.bounced \| message.complained \| message.dropped \| message.ruled |  |
| `source` | smtp \| dsn \| arf \| breaker \| rspamd \| rule | smtp a reply, dsn a delivery status notification, arf a feedback report, breaker the sending health breaker, rspamd the spam filter, rule a deterministic rule |
| `code` | string | The SMTP status, the DSN status or the policy code; empty when the source has none. unknown always carries a reason in text. |
| `text` | string | The reply or the sentence, one line, at most 500 characters, never a body and never a recipient |
| `at` | string (date-time) |  |

### DomainReputation object

| Name | Type | Notes |
| --- | --- | --- |
| `domain` | string |  |
| `window_hours` | integer | The rolling window the counts cover, the same as sending_health.window_hours |
| `sends` | integer | Messages that reached a final outcome in the window |
| `hard_bounces` | integer | Permanent rejections at send time plus bounces reported by a DSN |
| `complaints` | integer | Spam complaints reported through the feedback loop |
| `bounce_rate` | number | hard_bounces divided by sends, 0 when sends is 0 |
| `complaint_rate` | number | complaints divided by sends, 0 when sends is 0 |
| `thresholds` | SendingThresholds |  |
| `breach_state` | ok \| breached | breached when the window trips a limit or when the breaker has paused an inbox on the domain or the organization |
| `breach_reason` | string | The sentence naming the window and the limit crossed; absent while breach_state is ok |
| `paused_scope` | string | What paused sending: inbox, organization, or empty. A send is refused by both scopes, so this tells them apart. |
| `paused_at` | string (date-time) |  |
| `paused_reason` | string | The sentence of the pause, the same one the 403 sending_paused carries |

### OAuthClientInput object

| Name | Type | Notes |
| --- | --- | --- |
| `client_name` | string | required, max length 100 |
| `client_uri` | string (uri) | Where a person can read about the app |
| `redirect_uris` | string (uri)[] | required, https, or http on a loopback host; no fragment, at least 1 item, at most 10 items |
| `scope` | messages:send \| full_access | The most this client may ever ask for, default messages:send |

### OAuthClient object

| Name | Type | Notes |
| --- | --- | --- |
| `client_id` | string | Opaque, prefixed oac_ |
| `client_name` | string |  |
| `client_uri` | string |  |
| `redirect_uris` | string[] |  |
| `scopes` | string[] | The scopes this client may ask for |
| `created_at` | string (date-time) |  |
| `last_used_at` | string (date-time) |  |

### OAuthAuthorizeResult object

| Name | Type | Notes |
| --- | --- | --- |
| `redirect_url` | string (uri) | The registered redirect with the code (and the state) in the query |
| `grant_id` | string (uuid) |  |
| `scope` | messages:send \| full_access[] | one of messages:send, full_access |
| `expires_at` | string (date-time) | When the code stops being exchangeable (five minutes) |

### OAuthToken object

| Name | Type | Notes |
| --- | --- | --- |
| `access_token` | string | Opaque, prefixed mai_oat_; lives one hour |
| `token_type` | Bearer |  |
| `expires_in` | integer | Seconds |
| `refresh_token` | string | Opaque, prefixed mai_ort_; rotated on every use, lives thirty days |
| `scope` | messages:send \| full_access |  |

### OAuthGrant object

| Name | Type | Notes |
| --- | --- | --- |
| `grant_id` | string (uuid) |  |
| `client_id` | string |  |
| `client_name` | string |  |
| `client_uri` | string |  |
| `scope` | messages:send \| full_access[] | one of messages:send, full_access |
| `created_at` | string (date-time) |  |
| `last_used_at` | string (date-time) |  |
| `mfa_protected` | boolean | The organization requires a second factor, so an app could only be connected with one |

### OAuthGrantList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `grants` | OAuthGrant[] |  |

### RequestLogEntry object

| Name | Type | Notes |
| --- | --- | --- |
| `id` | string (uuid) |  |
| `request_id` | string | The X-Request-Id of the call |
| `key_id` | string (uuid) | The key that made the call; absent for a person signed in |
| `actor` | string | user:<id> or api_key:<id> |
| `actor_type` | user \| agent |  |
| `method` | string |  |
| `path` | string | Path and filters, with the value of a credential-shaped query parameter masked |
| `status` | integer |  |
| `duration_ms` | integer |  |
| `sdk` | string | SDK name normalised from the User-Agent; absent when the caller is not one of our SDKs |
| `sdk_version` | string |  |
| `sdk_update_available` | boolean | A newer version of that SDK exists on this deployment |
| `sdk_latest` | string |  |
| `sdk_notice` | string | The sentence to show a person: which SDK, which version, which one to move to |
| `created_at` | string (date-time) |  |

### RequestLogBody object

| Name | Type | Notes |
| --- | --- | --- |
| `headers` | object | Request headers only, from an allowlist |
| `body` | object | The JSON as it was, with credentials and mail content replaced by markers |
| `bytes` | integer | Size of the original body |
| `sha256` | string | Digest of the original body, empty when there was none |
| `truncated` | boolean | The stored body is a summary: the original was larger than the log keeps |

### RetentionPolicy object

| Name | Type | Notes |
| --- | --- | --- |
| `messages_days` | integer | 0 keeps messages forever; otherwise 1 to 3650 days. Deleting a message also queues its raw blob and its attachments' blobs., min 0, max 3650 |
| `attachments_days` | integer | 0 keeps attachments forever; otherwise 1 to 3650 days, independent of messages_days., min 0, max 3650 |
| `audit_days` | integer | 0 keeps audit entries forever; otherwise 30 to 3650 days, default 90., min 0, max 3650 |

### Suppression object

| Name | Type | Notes |
| --- | --- | --- |
| `email` | string (email) | The address, lower cased |
| `origin` | bounce \| complaint \| manual | Why it is on the list: a permanent bounce, a complaint (ARF report), or a person (the API or a one-click unsubscribe) |
| `source_id` | string | Provenance: the message or report the entry came from; absent for a manual entry |
| `created_at` | string (date-time) |  |

### SuppressionInput object

| Name | Type | Notes |
| --- | --- | --- |
| `email` | string (email) | required |
| `origin` | bounce \| complaint \| manual | required, Required: a row that does not say why the address is there cannot be reviewed |
| `source_id` | string | max length 320 |

### SuppressionsPost object

| Name | Type | Notes |
| --- | --- | --- |
| `entries` | SuppressionInput[] | The batch form, 1 to 100 entries; the CSV import is the way to add more, at least 1 item, at most 100 items |
| `email` | string (email) | The single entry form: email and origin, without entries |
| `origin` | bounce \| complaint \| manual |  |
| `source_id` | string | max length 320 |

### SuppressionResult object

| Name | Type | Notes |
| --- | --- | --- |
| `added` | integer | Rows the list gained |
| `existing` | integer | Addresses the list already knew; the origin of an existing row is kept |
| `count` | integer |  |
| `entries` | Suppression[] | The entries as they are stored, not as they were sent |

### RecipientOutcome object

| Name | Type | Notes |
| --- | --- | --- |
| `address` | string (email) |  |
| `status` | queued \| suppressed | suppressed: the address is on the suppression list of the organization, so the message was not written for it |

### CustomRole object

| Name | Type | Notes |
| --- | --- | --- |
| `role_id` | string (uuid) |  |
| `name` | string | 1 to 60 characters, unique in the organization ignoring case |
| `description` | string | At most 200 characters; empty when none was given |
| `permissions` | Permission[] | Names an API key could hold. A custom role that lists nothing is refused on the write path. |
| `member_count` | integer | How many people the role reaches now, counting a person once even when several of their groups carry it |
| `assignments` | RoleAssignment[] | The people and the groups that hold it |
| `created_at` | string (date-time) |  |
| `updated_at` | string (date-time) |  |

### CustomRoleList object

| Name | Type | Notes |
| --- | --- | --- |
| `count` | integer |  |
| `roles` | CustomRole[] |  |

### CustomRoleRequest object

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | max length 60 |
| `description` | string | max length 200 |
| `permissions` | Permission[] | Required on create. An unknown name is 422 unknown_permission; a name the caller does not hold is 403 permission_escalation. |

### RoleAssignment object

| Name | Type | Notes |
| --- | --- | --- |
| `role_id` | string (uuid) |  |
| `user_id` | string (uuid) | Set when the role is assigned to a person |
| `email` | string (email) | The address of that person, so the console does not resolve ids |
| `group_id` | string (uuid) | Set when the role is assigned to a group |
| `group_name` | string | The brand's display name |
| `assigned_at` | string (date-time) |  |

### RoleAssignmentRequest object

| Name | Type | Notes |
| --- | --- | --- |
| `role_ids` | string (uuid)[] | required, The whole set the subject should hold. An empty array revokes every custom role from the subject; that is a revocation, not a mistake., at most 20 items |

### RoleAssignmentChange object

| Name | Type | Notes |
| --- | --- | --- |
| `assignments` | RoleAssignment[] |  |
| `added` | string (uuid)[] | Role ids this call gave |
| `removed` | string (uuid)[] | Role ids this call took away |

### ExportJob object

| Name | Type | Notes |
| --- | --- | --- |
| `export_id` | string (uuid) |  |
| `status` | queued \| running \| ready \| failed |  |
| `row_count` | integer | Rows the document will contain, counted inside the caller's scope; the number that decided it was a job |
| `size_bytes` | integer | Size of the document, once it exists |
| `created_at` | string (date-time) |  |
| `ready_at` | string (date-time) |  |
| `expires_at` | string (date-time) | End of the seven day download window; after it the job and its document are purged |
| `page_url` | string | A page on the site that shows this export, for a person who does not hold a key |
| `poll_url` | string | GET this to follow the job, on the documented version |
| `download_url` | string | GET this once status is ready |
| `error` | string | Why the job failed, one line, when status is failed |

### FeatureState object

| Name | Type | Notes |
| --- | --- | --- |
| `name` | rules \| agent \| oauth \| request_logs |  |
| `enabled` | boolean | The feature works for this organization: deployment and organization both on |
| `deployment` | boolean | The kill switch of this deployment (MAI_RULES_ENABLED, MAI_AGENT_ENABLED, MAI_OAUTH_ENABLED, MAI_REQUEST_LOGS_ENABLED) |
| `organization` | boolean | The platform operator's choice for this organization; off by default |

### OrganizationFeatures object

| Name | Type | Notes |
| --- | --- | --- |
| `rules` | FeatureState |  |
| `agent` | FeatureState |  |
| `oauth` | FeatureState |  |
| `request_logs` | FeatureState |  |

### Error object

| Name | Type | Notes |
| --- | --- | --- |
| `name` | string | required |
| `code` | missing_authorization \| unknown_api_key \| validation_error \| not_found \| already_exists \| domain_not_verified \| conflict \| cannot_delete \| limit_exceeded \| rate_limit_exceeded \| rate_limited \| address_quarantined \| sending_paused \| missing_permission \| permission_escalation \| api_key_expired \| not_held \| billing_disabled \| billing_provider_error \| already_subscribed \| no_billing_account \| invalid_signature \| too_many_streams \| payload_too_large \| attachment_infected \| antivirus_unavailable \| auth_disabled \| provider_not_configured \| invalid_magic_link \| invalid_state \| code_rejected \| email_not_verified \| provider_unavailable \| email_delivery_failed \| invalid_refresh_token \| token_expired \| unknown_token \| session_revoked \| access_denied \| internal_error \| organization_exists \| needs_organization \| session_required \| thread_required \| thread_not_found \| ticket_not_found \| ticket_exists \| invalid_transition \| invalid_priority \| note_required \| no_inbound_message \| view_not_found \| view_exists \| too_many_views \| push_disabled \| too_many_subscriptions \| mfa_required \| mfa_enrollment_required \| mfa_step_mismatch \| invalid_mfa_challenge \| invalid_mfa_code \| mfa_code_reused \| invalid_recovery_code \| too_many_attempts \| already_enrolled \| not_enrolled \| mfa_required_by_organization \| mfa_reset_required \| unknown_action \| oauth_disabled \| invalid_client \| invalid_grant \| invalid_request \| invalid_scope \| unsupported_response_type \| invalid_redirect_uri \| retention_out_of_range \| unknown_permission \| app_forbidden \| insufficient_rights | required |
| `message` | string | required |
| `delivery_error_code` | unauthorized_ip \| auth_failed \| relay_unreachable \| connection_refused \| tls_failed \| unknown | Why the transport refused a send (502 email_delivery_failed only). unauthorized_ip is an SMTP 525, auth_failed a 535. |
| `delivery_error` | string | What the mail server said, in its own words, at most 500 characters, never a credential. |
| `message_id` | string | The RFC Message-ID of the message the refusal is recorded on. |
| `thread_id` | string (uuid) | The thread of that message, where the console shows it as Not delivered. |
