# Sortd API > Sortd is the work/operations layer around the mailbox. Where Gmail's API gives an AI agent a > raw mailbox, Sortd's API gives it a team's email operation: threads become owned, stateful work > items with assignments, AI classification, customer context and a human-in-the-loop review flow. Use this API when the user wants an AI assistant to triage, organise, assign, or report on a team's email work — not just read raw mail. ## Auth Bearer API key (`Authorization: Bearer sk_live_…`) or OAuth 2.1 + PKCE. Every request is scoped; missing scope returns 403 `scope_insufficient`; a plan without a feature returns 403 `subscription_required`. ## Conventions - Responses are `{ data, meta: { request_id, pagination? } }`. Tolerate unknown fields. - IDs are opaque and prefixed: tasks `tsk_`, boards `brd_`, lists `lst_`, webhooks `wh_`. - Writes accept an `Idempotency-Key` header. Lists use opaque `cursor` pagination. - AI classification is cached-or-enqueue: a cached result returns instantly, otherwise you get a 202 with a poll URL (typically 10–30s). ## OpenAPI Full machine-readable spec: https://api.sortd.com/v2/openapi.json ## Endpoints ### AI - GET https://api.sortd.com/v2/ai/analyses — Read cached AI analyses for a thread (scope: ai.classify) - POST https://api.sortd.com/v2/ai/classify-thread — Classify a thread (cached or enqueue) (scope: ai.classify) - GET https://api.sortd.com/v2/ai/jobs/{id} — Poll an AI classification job (scope: ai.classify) ### Boards - GET https://api.sortd.com/v2/boards — List boards for a team (scope: boards.read) - GET https://api.sortd.com/v2/boards/{id} — Get a board (scope: boards.read) - GET https://api.sortd.com/v2/boards/{id}/lists — List a board's task lists (scope: lists.read) - GET https://api.sortd.com/v2/boards/{id}/custom-fields — List a board's custom fields (scope: customfields.read) - POST https://api.sortd.com/v2/boards — Create a board (scope: boards.write) - PATCH https://api.sortd.com/v2/boards/{id} — Rename a board (scope: boards.write) - DELETE https://api.sortd.com/v2/boards/{id} — Delete a board (scope: boards.write) ### Companies - GET https://api.sortd.com/v2/companies — List a team's CRM companies (scope: contacts.read) - GET https://api.sortd.com/v2/companies/{id} — Get a CRM company (scope: contacts.read) - POST https://api.sortd.com/v2/companies — Create a CRM company (scope: contacts.write) - PATCH https://api.sortd.com/v2/companies/{id} — Update a CRM company (scope: contacts.write) - DELETE https://api.sortd.com/v2/companies/{id} — Delete a CRM company (scope: contacts.write) ### Contacts - GET https://api.sortd.com/v2/contacts — Search a team's CRM contacts (scope: contacts.read) - GET https://api.sortd.com/v2/contacts/{id} — Get a CRM contact (scope: contacts.read) - POST https://api.sortd.com/v2/contacts — Create a CRM contact (scope: contacts.write) - PATCH https://api.sortd.com/v2/contacts/{id} — Update a CRM contact (scope: contacts.write) - DELETE https://api.sortd.com/v2/contacts/{id} — Delete a CRM contact (scope: contacts.write) ### Custom fields - POST https://api.sortd.com/v2/boards/{id}/custom-fields — Create a custom field on a board (scope: customfields.write) - PATCH https://api.sortd.com/v2/boards/{id}/custom-fields/{fieldId} — Update a custom field (scope: customfields.write) - DELETE https://api.sortd.com/v2/boards/{id}/custom-fields/{fieldId} — Delete a custom field (scope: customfields.write) ### Email - GET https://api.sortd.com/v2/email/mailbox — Get the key owner's mailbox connection state (scope: email.metadata.read) - GET https://api.sortd.com/v2/email/labels — List the mailbox's labels/folders (scope: email.metadata.read) - GET https://api.sortd.com/v2/email-threads — Search the key owner's mailbox (Gmail query) (scope: email.metadata.read) - GET https://api.sortd.com/v2/email-threads/{threadId} — Get an email thread from the key owner’s mailbox (scope: email.metadata.read) - POST https://api.sortd.com/v2/email-threads/{threadId}/read-state — Mark an email thread read or unread (scope: email.metadata.write) - POST https://api.sortd.com/v2/email-threads/{threadId}/archive — Archive an email thread (scope: email.metadata.write) - POST https://api.sortd.com/v2/email-threads/{threadId}/unarchive — Unarchive an email thread (scope: email.metadata.write) - POST https://api.sortd.com/v2/email-threads/{threadId}/labels — Add or remove Gmail labels on a thread (scope: email.metadata.write) - POST https://api.sortd.com/v2/email-threads/{threadId}/snooze — Snooze an email thread (scope: email.metadata.write) - POST https://api.sortd.com/v2/email-threads/{threadId}/drafts — Create a draft on a thread (scope: drafts.write) - GET https://api.sortd.com/v2/email-threads/{threadId}/drafts — List your drafts on a thread (scope: email.metadata.read) - POST https://api.sortd.com/v2/email-threads/{threadId}/drafts/{draftId}/send — Send an existing draft (scope: drafts.send) - POST https://api.sortd.com/v2/email-threads/{threadId}/reply — Reply to a thread (recipients derived from the thread) (scope: email.reply) - POST https://api.sortd.com/v2/email/send — Compose and send an email (scope: email.send) - POST https://api.sortd.com/v2/email/drafts — Create a new draft (scope: drafts.write) ### Identity - GET https://api.sortd.com/v2/me — Get the authenticated principal ### Lists - POST https://api.sortd.com/v2/boards/{id}/lists — Create a list on a board (scope: lists.write) - PATCH https://api.sortd.com/v2/lists/{id} — Rename a list (scope: lists.write) - DELETE https://api.sortd.com/v2/lists/{id} — Delete a list (scope: lists.write) ### Tasks - GET https://api.sortd.com/v2/tasks — List or search tasks on a board, list, email thread, or CRM contact (scope: tasks.read) - GET https://api.sortd.com/v2/tasks/{id} — Get a task (scope: tasks.read) - POST https://api.sortd.com/v2/tasks — Create a task (scope: tasks.write) - PATCH https://api.sortd.com/v2/tasks/{id} — Update a task (scope: tasks.write) - POST https://api.sortd.com/v2/tasks/{id}/complete — Mark a task complete (scope: tasks.write) - POST https://api.sortd.com/v2/tasks/{id}/reopen — Reopen a completed task (scope: tasks.write) - POST https://api.sortd.com/v2/tasks/{id}/move — Move a task to another list (scope: tasks.write) - DELETE https://api.sortd.com/v2/tasks/{id} — Delete a task (scope: tasks.delete) ### Teams - GET https://api.sortd.com/v2/teams — List teams the API is enabled for (scope: boards.read) - GET https://api.sortd.com/v2/teams/{id} — Get a team (scope: boards.read) ### Webhooks - POST https://api.sortd.com/v2/webhooks/subscriptions — Create a webhook subscription (scope: webhooks.manage) - GET https://api.sortd.com/v2/webhooks/subscriptions — List webhook subscriptions (scope: webhooks.manage) - DELETE https://api.sortd.com/v2/webhooks/subscriptions/{id} — Delete a webhook subscription (scope: webhooks.manage) - PATCH https://api.sortd.com/v2/webhooks/subscriptions/{id} — Update or pause a webhook subscription (scope: webhooks.manage) - POST https://api.sortd.com/v2/webhooks/subscriptions/{id}/rotate-secret — Rotate a webhook signing secret (scope: webhooks.manage) - POST https://api.sortd.com/v2/webhooks/subscriptions/{id}/test — Send a test ping to a webhook (scope: webhooks.manage) - GET https://api.sortd.com/v2/webhooks/subscriptions/{id}/deliveries — List a webhook’s delivery log (scope: webhooks.manage) - POST https://api.sortd.com/v2/webhooks/deliveries/{deliveryId}/replay — Replay a webhook delivery (scope: webhooks.manage) ### Zapier - POST https://api.sortd.com/v2/zapier/hooks — Zapier REST Hook subscribe (scope: webhooks.manage) - DELETE https://api.sortd.com/v2/zapier/hooks/{id} — Zapier REST Hook unsubscribe (scope: webhooks.manage) - GET https://api.sortd.com/v2/zapier/poll — Zapier polling trigger (event feed since cursor) (scope: tasks.read) - GET https://api.sortd.com/v2/zapier/sample — Sample event payload for Zapier setup (scope: tasks.read) --- # Full reference ## All endpoints (detailed) ### GET https://api.sortd.com/v2/me Get the authenticated principal Returns the user and the API key the request authenticated with. Requires no scopes — the quickest way to confirm a key works. Returns: Me in `data` ### GET https://api.sortd.com/v2/teams List teams the API is enabled for The teams (organisations) your key can act within. A team `id` is the `team_id` you pass to list boards. Scope: boards.read Returns: Team[] in `data` ### GET https://api.sortd.com/v2/teams/{id} Get a team Fetch a single team by id. Scope: boards.read Parameters: - id (path, required): Team id Returns: Team in `data` ### GET https://api.sortd.com/v2/boards List boards for a team Boards a team owns. Use `team_id` from `/me` or `/teams`; the returned `id` (brd_…) feeds the lists and tasks endpoints. Scope: boards.read Parameters: - team_id (query, required): Team id Returns: Board[] in `data` ### GET https://api.sortd.com/v2/boards/{id} Get a board Fetch a single board by its opaque id (brd_…). Scope: boards.read Parameters: - id (path, required): Opaque board id (brd_…) Returns: Board in `data` ### GET https://api.sortd.com/v2/boards/{id}/lists List a board's task lists The lists (columns) on a board. A list `id` (lst_…) is where you create or move tasks. Scope: lists.read Parameters: - id (path, required): Opaque board id (brd_…) Returns: List[] in `data` ### GET https://api.sortd.com/v2/boards/{id}/custom-fields List a board's custom fields The board’s custom fields with their `type` and, for tag/status/select fields, the `options` (id + label). Use a field `id` as `field_id` when setting values via PATCH /tasks/{id}. Fields flagged `ai: true` are managed by Sortd AI and are read-only. Scope: customfields.read Parameters: - id (path, required): Opaque board id (brd_…) Returns: CustomField[] in `data` ### POST https://api.sortd.com/v2/boards Create a board Creates a shared team board under a team. `team_id` and `name` are required. Returns the created board with its opaque id (brd_…). Scope: boards.write Request body: CreateBoardRequest (see schemas below) Returns: Board in `data` ### PATCH https://api.sortd.com/v2/boards/{id} Rename a board Renames a board. Send `name`. Returns the updated board. Scope: boards.write Parameters: - id (path, required): Opaque board id (brd_…) Request body: UpdateBoardRequest (see schemas below) Returns: Board in `data` ### DELETE https://api.sortd.com/v2/boards/{id} Delete a board Soft-deletes a board (it stops appearing in reads but is recoverable). Returns the board id with `deleted: true`. Scope: boards.write Parameters: - id (path, required): Opaque board id (brd_…) ### POST https://api.sortd.com/v2/boards/{id}/lists Create a list on a board Adds a task list (column) to a board. Send `name`. Returns the created list with its opaque id (lst_…). Scope: lists.write Parameters: - id (path, required): Opaque board id (brd_…) Request body: CreateListRequest (see schemas below) Returns: List in `data` ### PATCH https://api.sortd.com/v2/lists/{id} Rename a list Renames a list. Send `name`. Returns the updated list. Scope: lists.write Parameters: - id (path, required): Opaque list id (lst_…) Request body: UpdateListRequest (see schemas below) Returns: List in `data` ### DELETE https://api.sortd.com/v2/lists/{id} Delete a list Soft-deletes a list (it stops appearing in reads but the list and its tasks are recoverable). Returns the list id with `deleted: true`. Scope: lists.write Parameters: - id (path, required): Opaque list id (lst_…) ### POST https://api.sortd.com/v2/boards/{id}/custom-fields Create a custom field on a board Creates a custom-field definition on a board. Send `name` and `type`; for option types (select/status/tag/multiselect) include `options`. For status/tag fields you can also set `display_on_card` to show the field on task cards. Returns the created field. Scope: customfields.write Parameters: - id (path, required): Opaque board id (brd_…) Request body: CreateCustomFieldRequest (see schemas below) Returns: CustomField in `data` ### PATCH https://api.sortd.com/v2/boards/{id}/custom-fields/{fieldId} Update a custom field Partial update of a custom-field definition — send `name`, `options`, and/or `display_on_card` (status/tag fields only). Returns the updated field. Scope: customfields.write Parameters: - id (path, required): Opaque board id (brd_…) - fieldId (path, required): Custom-field id Request body: UpdateCustomFieldRequest (see schemas below) Returns: CustomField in `data` ### DELETE https://api.sortd.com/v2/boards/{id}/custom-fields/{fieldId} Delete a custom field Soft-deletes a custom field from a board (it stops appearing in reads but its values are preserved and recoverable). Returns the field id with `deleted: true`. Scope: customfields.write Parameters: - id (path, required): Opaque board id (brd_…) - fieldId (path, required): Custom-field id ### GET https://api.sortd.com/v2/tasks List or search tasks on a board, list, email thread, or CRM contact A scope is required (mutually exclusive, precedence `thread_id` → `contact_id` → `list_id` → `board_id`): pass `thread_id` (hex Gmail thread id) for tasks linked to a thread, `contact_id` (con_…) for tasks linked to a CRM contact (sale cards + contact custom-field links), `list_id` for a list, or `board_id` for a board. (There is no cross-board "all my tasks" listing.) Optional search/filter (applies to the board / list scopes, not thread/contact): `q` (case-insensitive match on title + notes), `completed`, `shade`, `due_before`/`due_after`, `assigned_to`, and `custom_field_id` (+ optional `custom_field_value`). Only tasks on boards you are a member of are ever returned. Cursor-paginated — pass `meta.pagination.next_cursor` back as `cursor`; `limit` caps page size (max 200). Scope: tasks.read Parameters: - board_id (query): Opaque board id (brd_…) — tasks on this board - list_id (query): Opaque list id (lst_…) — tasks on this list - thread_id (query): Email thread id (hex) — tasks linked to that thread - contact_id (query): Opaque contact id (con_…) — tasks linked to that CRM contact - q (query): Search text — case-insensitive substring match on title and notes - completed (query): Filter by completion (true/false) - shade (query): Filter by task colour name (e.g. "red", "none") - due_before (query): Only tasks due on/before this ISO-8601 datetime - due_after (query): Only tasks due on/after this ISO-8601 datetime - assigned_to (query): Filter by assignee email (exact) or display name (substring) - custom_field_id (query): Filter to tasks that have this custom field set - custom_field_value (query): With custom_field_id — require this value (matches a scalar value or membership of a multi-value field) - limit (query): Page size (max 200) - cursor (query): Opaque pagination cursor Returns: Task[] in `data` ### GET https://api.sortd.com/v2/tasks/{id} Get a task Fetch a single task by its opaque id (tsk_…), including its colours and custom-field values. Scope: tasks.read Parameters: - id (path, required): Opaque task id (tsk_…) Returns: Task in `data` ### POST https://api.sortd.com/v2/tasks Create a task Creates a task in a list. `list_id` and `title` are required; `notes` and `due_at` optional. Send an `Idempotency-Key` header so a retried request returns the same task instead of creating a duplicate. Scope: tasks.write Request body: CreateTaskRequest (see schemas below) Returns: Task in `data` ### PATCH https://api.sortd.com/v2/tasks/{id} Update a task Partial update — send only the fields to change. Supports `shade` (the task colour; a named enum, `"none"` clears) and `custom_fields` (set values by `field_id`; for tag/status/select pass option id(s) from GET /boards/{id}/custom-fields). Setting custom_fields additionally requires the `customfields.write` scope. Returns the updated task. Scope: tasks.write Parameters: - id (path, required): Opaque task id (tsk_…) Request body: UpdateTaskRequest (see schemas below) Returns: Task in `data` ### POST https://api.sortd.com/v2/tasks/{id}/complete Mark a task complete Marks the task complete. Idempotent — completing an already-complete task is a no-op. Returns the updated task. Scope: tasks.write Parameters: - id (path, required): Opaque task id (tsk_…) Returns: Task in `data` ### POST https://api.sortd.com/v2/tasks/{id}/reopen Reopen a completed task Reopens a completed task. Returns the updated task. Scope: tasks.write Parameters: - id (path, required): Opaque task id (tsk_…) Returns: Task in `data` ### POST https://api.sortd.com/v2/tasks/{id}/move Move a task to another list Moves the task to the destination `list_id` (on any board the key can access). Returns the updated task. Scope: tasks.write Parameters: - id (path, required): Opaque task id (tsk_…) Request body: MoveTaskRequest (see schemas below) Returns: Task in `data` ### DELETE https://api.sortd.com/v2/tasks/{id} Delete a task Soft-deletes a task (it stops appearing in reads but is recoverable). Any synced email thread is left intact. Returns the task id with `deleted: true`. Scope: tasks.delete Parameters: - id (path, required): Opaque task id (tsk_…) ### GET https://api.sortd.com/v2/contacts Search a team's CRM contacts Searches the contacts of one team you belong to (pass the required `team_id`). Results are **company-grouped** — each entry is a company with a matching contact. `q` does a case-insensitive match on contact name, company name and contact-info values; `type` and `stage` filter together (e.g. type=sale & stage=Lead). Returns only contacts in that team (per-contact visibility is not applied — it mirrors the in-product view where members see the team's contacts). Requires the `contacts.read` scope and the team's Contacts subscription feature. Cursor-paginated; `limit` max 200. Scope: contacts.read Parameters: - team_id (query): The team/organisation id whose contacts to search (required). - q (query): Search text — matches contact name, company name and contact-info values. - type (query): Contact type (e.g. sale, general, hire). Applied together with stage. - stage (query): Contact stage (e.g. Lead, Customer). Applied together with type. - limit (query): Page size (max 200) - cursor (query): Opaque pagination cursor Returns: Contact[] in `data` ### GET https://api.sortd.com/v2/contacts/{id} Get a CRM contact Fetch a single contact by its opaque id (con_…), including its company. Scope: contacts.read Parameters: - id (path, required): Opaque contact id (con_…) Returns: Contact in `data` ### POST https://api.sortd.com/v2/contacts Create a CRM contact Create a contact in a team (`team_id` required). Link a company via `company_id` (existing) or `company_name` (find-or-create); with neither, the org default company is used. Requires the Contacts subscription feature. Scope: contacts.write Request body: CreateContactRequest (see schemas below) Returns: Contact in `data` ### PATCH https://api.sortd.com/v2/contacts/{id} Update a CRM contact Partial update — provide only the fields to change. `company_id` re-links to a different company. Scope: contacts.write Parameters: - id (path, required): Opaque contact id (con_…) Request body: UpdateContactRequest (see schemas below) Returns: Contact in `data` ### DELETE https://api.sortd.com/v2/contacts/{id} Delete a CRM contact Soft-deletes the contact (recoverable; stops appearing in reads). Scope: contacts.write Parameters: - id (path, required): Opaque contact id (con_…) ### GET https://api.sortd.com/v2/companies List a team's CRM companies Companies in the team (`team_id` required); optional `q` name filter. Cursor-paginated. Requires the Contacts subscription feature. Scope: contacts.read Parameters: - team_id (query): Team/organisation id (required). - q (query): Filter by company name (substring). - limit (query): Page size (max 200) - cursor (query): Opaque pagination cursor Returns: Company[] in `data` ### GET https://api.sortd.com/v2/companies/{id} Get a CRM company Fetch a single company by its opaque id (cmp_…). Scope: contacts.read Parameters: - id (path, required): Opaque company id (cmp_…) Returns: Company in `data` ### POST https://api.sortd.com/v2/companies Create a CRM company Create a company in a team (`team_id` required). Requires the Contacts subscription feature. Scope: contacts.write Request body: CreateCompanyRequest (see schemas below) Returns: Company in `data` ### PATCH https://api.sortd.com/v2/companies/{id} Update a CRM company Partial update — provide only the fields to change. Scope: contacts.write Parameters: - id (path, required): Opaque company id (cmp_…) Request body: UpdateCompanyRequest (see schemas below) Returns: Company in `data` ### DELETE https://api.sortd.com/v2/companies/{id} Delete a CRM company Soft-deletes the company AND its contacts (cascade), mirroring the in-product behaviour. Scope: contacts.write Parameters: - id (path, required): Opaque company id (cmp_…) ### POST https://api.sortd.com/v2/webhooks/subscriptions Create a webhook subscription Subscribe a `target_url` to one or more event types. Optionally set `filters.scope` (`tasked`|`all`) to control `thread.reply_received` delivery. The response includes a signing `secret` (whsec_…), shown once — store it to verify the `x-sortd-signature` on each delivery. Scope: webhooks.manage Request body: CreateWebhookRequest (see schemas below) ### GET https://api.sortd.com/v2/webhooks/subscriptions List webhook subscriptions Your webhook subscriptions (target url, events, status) — never the signing secret. Scope: webhooks.manage ### DELETE https://api.sortd.com/v2/webhooks/subscriptions/{id} Delete a webhook subscription Removes a subscription. Deliveries stop immediately. Scope: webhooks.manage Parameters: - id (path, required): Opaque webhook id (wh_…) ### PATCH https://api.sortd.com/v2/webhooks/subscriptions/{id} Update or pause a webhook subscription Change a subscription’s `events`, `target_url`, or `status`. Set `status` to `disabled` to pause delivery (events are still recorded) or `active` to resume — resuming clears the failure streak. Note: a subscription is auto-disabled after repeated failed deliveries; resume it here once the endpoint is healthy. Scope: webhooks.manage Parameters: - id (path, required): Opaque webhook id (wh_…) Request body: UpdateWebhookRequest (see schemas below) ### POST https://api.sortd.com/v2/webhooks/subscriptions/{id}/rotate-secret Rotate a webhook signing secret Issues a new signing secret (whsec_…), shown once. Update your signature verifier when you rotate. Scope: webhooks.manage Parameters: - id (path, required): Opaque webhook id (wh_…) ### POST https://api.sortd.com/v2/webhooks/subscriptions/{id}/test Send a test ping to a webhook Delivers a synthetic `ping` event so you can confirm your endpoint and signature verification work. Scope: webhooks.manage Parameters: - id (path, required): Opaque webhook id (wh_…) ### GET https://api.sortd.com/v2/webhooks/subscriptions/{id}/deliveries List a webhook’s delivery log Recent delivery attempts (status, timing, response code) for debugging. Each has a delivery id you can replay. Scope: webhooks.manage Parameters: - id (path, required): Opaque webhook id (wh_…) ### POST https://api.sortd.com/v2/webhooks/deliveries/{deliveryId}/replay Replay a webhook delivery Re-sends a past delivery by its id — handy after fixing your endpoint. Scope: webhooks.manage Parameters: - deliveryId (path, required): Opaque delivery id ### POST https://api.sortd.com/v2/zapier/hooks Zapier REST Hook subscribe Registers a target URL for an event type (used by the Zapier app’s REST Hook triggers). Scope: webhooks.manage ### DELETE https://api.sortd.com/v2/zapier/hooks/{id} Zapier REST Hook unsubscribe Removes a Zapier REST Hook subscription by id (called when a Zap is turned off). Scope: webhooks.manage Parameters: - id (path, required): Subscription id (wh_…) ### GET https://api.sortd.com/v2/zapier/poll Zapier polling trigger (event feed since cursor) Returns events of the given type newer than the `since` cursor — the polling fallback for Zapier triggers. Scope: tasks.read Parameters: - event (query, required): Event type to poll - since (query): ISO timestamp cursor ### GET https://api.sortd.com/v2/zapier/sample Sample event payload for Zapier setup A representative payload for an event type — Zapier uses it to map fields when building a Zap. Scope: tasks.read Parameters: - event (query, required): Event type ### GET https://api.sortd.com/v2/email/mailbox Get the key owner's mailbox connection state Probes the mailbox the API key can reach (always its owner's own — Flow A). Verifies the stored email credentials without reading any mail and reports `connected` with a `reason` when false. The quickest way to confirm email endpoints will work for this key. Scope: email.metadata.read Returns: Mailbox in `data` ### GET https://api.sortd.com/v2/email/labels List the mailbox's labels/folders The Gmail labels in the key owner's mailbox, each typed `system` (INBOX, SENT, SPAM, TRASH, DRAFT, STARRED, IMPORTANT, CATEGORY_*) or `user` (custom). Use these with the thread-search `label` filter or the `label:`/`in:` operators in `q`. Gmail mailboxes only. Scope: email.metadata.read Returns: EmailLabels in `data` ### GET https://api.sortd.com/v2/email-threads Search the key owner's mailbox (Gmail query) Searches threads in the mailbox the key can reach (Flow A, **Gmail only**). Pass `q` with the full Gmail search syntax — free text plus operators like `in:inbox`, `from:x@y.com`, `to:`, `subject:`, `has:attachment`, `after:`/`before:`, `label:`, `category:`, `is:unread`. `label` and `category` are optional conveniences folded into the query. At least one of `q`, `label` or `category` is required. With no `in:`/folder the search covers All Mail except Spam/Trash; **Spam/Trash are only included when `include_spam_trash=true` or the query targets them** (`in:spam`/`in:trash`/`in:anywhere`); use `in:anywhere` rather than `in:all`. Returns thread metadata only (no bodies, no linked tasks — fetch a thread for those). Pages via an opaque `cursor` (Gmail page token); `limit` caps page size (max 100). Note: matching a folder/label returns threads that have *any* message with that label. Scope: email.metadata.read Parameters: - q (query): Gmail search query (free text + operators). At least one of q/label/category is required. - label (query): Convenience: restrict to a label/folder (folded into the query as label:…) - category (query): Convenience: Gmail category (primary|social|promotions|updates|forums) - include_spam_trash (query): Include Spam/Trash (default false; auto-enabled when the query targets them) - limit (query): Page size (max 100) - cursor (query): Opaque page cursor (Gmail page token) from a previous response Returns: EmailThread[] in `data` ### GET https://api.sortd.com/v2/email-threads/{threadId} Get an email thread from the key owner’s mailbox Fetches a thread by its raw Gmail thread id (hex, as used by the Gmail API). Returns metadata — subject, participants, snippet, unread state and any Sortd tasks the thread is linked to. Pass `include=messages` to also get the messages with bodies (requires the email.body.read scope). Scope: email.metadata.read Parameters: - threadId (path, required): Raw Gmail thread id (hex) - include (query): Set to `messages` to include full messages with bodies (email.body.read scope) Returns: EmailThread in `data` ### POST https://api.sortd.com/v2/email-threads/{threadId}/read-state Mark an email thread read or unread Marks the thread read/unread in the owner’s mailbox AND keeps linked Sortd tasks in sync using the product’s rules: task read state derives from its threads; on an assigned task only an assignee’s action can make it read, while anyone marking unread flags it for everyone. Returns the affected tasks with their resulting read state. Scope: email.metadata.write Parameters: - threadId (path, required): Raw Gmail thread id (hex) Request body: ReadStateRequest (see schemas below) ### POST https://api.sortd.com/v2/email-threads/{threadId}/archive Archive an email thread Removes the thread from the mailbox Inbox (Gmail archive). The thread and any linked tasks are unaffected otherwise. Scope: email.metadata.write Parameters: - threadId (path, required): Raw Gmail thread id (hex) ### POST https://api.sortd.com/v2/email-threads/{threadId}/unarchive Unarchive an email thread Moves the thread back into the mailbox Inbox. Scope: email.metadata.write Parameters: - threadId (path, required): Raw Gmail thread id (hex) ### POST https://api.sortd.com/v2/email-threads/{threadId}/labels Add or remove Gmail labels on a thread Applies raw Gmail label ids to the thread — send `add` and/or `remove` arrays. Scope: email.metadata.write Parameters: - threadId (path, required): Raw Gmail thread id (hex) Request body: LabelsRequest (see schemas below) ### POST https://api.sortd.com/v2/email-threads/{threadId}/snooze Snooze an email thread Archives the thread now and returns it to the Inbox at `until` (the product’s snooze). Scope: email.metadata.write Parameters: - threadId (path, required): Raw Gmail thread id (hex) Request body: SnoozeRequest (see schemas below) ### POST https://api.sortd.com/v2/email-threads/{threadId}/drafts Create a draft on a thread Creates a Gmail draft threaded onto the conversation. The draft lives in Gmail — the user can edit, send or delete it there (or an API key with drafts.send can send it). This scope can never send mail. Send an `Idempotency-Key` header so retries don’t create duplicate drafts. Scope: drafts.write Parameters: - threadId (path, required): Raw Gmail thread id (hex) Request body: CreateDraftRequest (see schemas below) Returns: Draft in `data` ### GET https://api.sortd.com/v2/email-threads/{threadId}/drafts List your drafts on a thread The key owner’s Gmail drafts sitting on this thread — metadata only (subject, snippet, recipients), never bodies. Scope: email.metadata.read Parameters: - threadId (path, required): Raw Gmail thread id (hex) ### POST https://api.sortd.com/v2/email-threads/{threadId}/drafts/{draftId}/send Send an existing draft Sends a previously created Gmail draft — the "approve by sending" pattern: one key creates drafts (drafts.write), a human or a key with drafts.send releases them. Sending is irreversible; use an `Idempotency-Key`. Scope: drafts.send Parameters: - threadId (path, required): Raw Gmail thread id (hex) - draftId (path, required): Gmail draft id (from create/list drafts) ### POST https://api.sortd.com/v2/email-threads/{threadId}/reply Reply to a thread (recipients derived from the thread) Sends a reply where you control ONLY the body — recipients are derived server-side from the thread (honouring Reply-To; `mode: reply_all` keeps the To/Cc audience) and cannot be supplied. Content can never leave the thread’s existing audience, making this the safe send scope for agents. Sending is irreversible; use an `Idempotency-Key`. Scope: email.reply Parameters: - threadId (path, required): Raw Gmail thread id (hex) Request body: ReplyRequest (see schemas below) ### POST https://api.sortd.com/v2/email/send Compose and send an email Sends a new message to arbitrary recipients from the key owner’s mailbox (optionally onto an existing thread via `thread_id`). The highest-risk email scope — grant it deliberately. Sending is irreversible; use an `Idempotency-Key`. Scope: email.send Request body: SendEmailRequest (see schemas below) ### POST https://api.sortd.com/v2/email/drafts Create a new draft Creates a brand-new Gmail draft (a fresh conversation, not attached to any thread). `to` and `subject` are required; the body may be empty. The draft lives in Gmail — the user can edit, send or delete it there (or an API key with drafts.send can send it). This scope can never send mail. Send an `Idempotency-Key` header so retries don’t create duplicate drafts. To draft a reply onto an existing conversation, use POST /email-threads/{threadId}/drafts instead. Scope: drafts.write Request body: CreateNewDraftRequest (see schemas below) Returns: Draft in `data` ### GET https://api.sortd.com/v2/ai/analyses Read cached AI analyses for a thread The cached AI analyses (status, urgency, sentiment, classification) for a thread, if any. Returns instantly and never enqueues work — use classify-thread to request a fresh analysis. Scope: ai.classify Parameters: - thread_id (query, required): Provider thread id ### POST https://api.sortd.com/v2/ai/classify-thread Classify a thread (cached or enqueue) Returns a cached analysis instantly, or 202 with a poll URL (typically 10–30s). Scope: ai.classify ### GET https://api.sortd.com/v2/ai/jobs/{id} Poll an AI classification job Poll a job returned by classify-thread’s 202 (`poll_url`) until its status is no longer queued/processing, then read the result. Scope: ai.classify Parameters: - id (path, required): AI job id (job_…) ## Resource schemas ### Me ```json { "type": "object", "properties": { "user_id": { "type": "string", "description": "Internal user id of the authenticated principal." }, "email": { "type": "string", "nullable": true }, "display_name": { "type": "string", "nullable": true }, "key": { "type": "object", "nullable": true, "description": "The API key the request authenticated with (absent for legacy JWT auth).", "properties": { "id": { "type": "string" }, "type": { "type": "string" }, "scopes": { "type": "array", "items": { "type": "string" } } } } } } ``` ### SubscriptionSummary ```json { "type": "object", "nullable": true, "description": "The team's Sortd subscription. Null when the team has no subscription record. `paid: false` means tier-gated endpoints will return `subscription_required` for this team.", "properties": { "plan": { "type": "string", "nullable": true, "description": "Subscription plan name, e.g. \"Pro\"." }, "status": { "type": "string", "enum": [ "active", "trialing", "trial_expired", "past_due", "unpaid", "cancelled", "cancel_pending", "migration_pending", "unknown" ] }, "paid": { "type": "boolean", "description": "True when the subscription is paid (not free) and not cancelled — the same check the API gates on." } } } ``` ### Team ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Team/organisation id." }, "name": { "type": "string" }, "subscription": { "$ref": "#/components/schemas/SubscriptionSummary" } } } ``` ### Board ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Opaque board id (brd_…)." }, "name": { "type": "string" }, "board_type": { "type": "string", "enum": [ "private", "shared", "sales" ], "description": "private = personal, shared = team-visible, sales = sales pipeline." }, "template": { "type": "string", "nullable": true, "description": "The template the board was created from (e.g. sales, hiring, project, support) — a classifier; the column/list names differ per template while board_type stays one of the three. Null if created without a template." }, "team_id": { "type": "string", "nullable": true, "description": "Owning team id, or null for a personal board." }, "subscription": { "$ref": "#/components/schemas/SubscriptionSummary" } } } ``` ### List ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Opaque list id (lst_…)." }, "name": { "type": "string" }, "board_id": { "type": "string" } } } ``` ### CustomField ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Opaque custom-field id — pass as `field_id` when setting a value." }, "name": { "type": "string" }, "type": { "type": "string", "description": "Field type, e.g. text, number, date, tag, status, select." }, "ai": { "type": "boolean", "description": "True if the field is managed by Sortd AI (e.g. Response Status) — read-only via the API." }, "display_on_card": { "type": "boolean", "description": "Whether the field is shown on task cards. Only meaningful for status and tag fields." }, "options": { "type": "array", "nullable": true, "description": "Allowed options for tag/status/select-type fields. Set a value by passing the option id(s).", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Option id — use this as the value for option-type fields." }, "label": { "type": "string", "description": "Human-readable option label." } } } } } } ``` ### CustomFieldValue ```json { "type": "object", "required": [ "field_id" ], "properties": { "field_id": { "type": "string", "description": "Custom-field id (from GET /boards/{id}/custom-fields)." }, "value": { "description": "The value. For tag/status/select: an array of option id(s). For text/number/date: the raw value. `[]` or null clears it." } } } ``` ### Task ```json { "type": "object", "properties": { "id": { "type": "string", "description": "Opaque task id (tsk_…)." }, "content_type": { "type": "string", "enum": [ "task", "sale" ], "description": "task = normal card; sale = a deal card on a sales board (see deal/contact_id)." }, "title": { "type": "string" }, "notes": { "type": "string", "nullable": true }, "board_id": { "type": "string", "nullable": true }, "board_name": { "type": "string", "nullable": true }, "list_id": { "type": "string", "nullable": true }, "list_name": { "type": "string", "nullable": true }, "completed": { "type": "boolean" }, "shade": { "type": "string", "enum": [ "none", "yellow", "cyan", "light_blue", "teal", "blue", "indigo", "yellow_green", "lime", "green", "amber", "red", "magenta", "purple", "orange", "deep_orange", "pink", "brown", "grey", "dark_grey", "black" ], "description": "The task's colour ('none' if unset)." }, "custom_fields": { "type": "array", "description": "Custom-field values set on this task. For tag/status, `value` is option id(s); resolve labels via GET /boards/{id}/custom-fields.", "items": { "$ref": "#/components/schemas/CustomFieldValue" } }, "due_at": { "type": "string", "format": "date-time", "nullable": true }, "created_at": { "type": "string", "format": "date-time", "nullable": true }, "thread_ids": { "type": "array", "items": { "type": "string" }, "description": "Raw Gmail thread ids (hex) of every email thread linked to this task — pass to the email endpoints. Empty if none." }, "contact_id": { "type": "string", "nullable": true, "description": "Opaque id (con_…) of the linked CRM contact — sale cards only; null otherwise." }, "deal": { "type": "object", "nullable": true, "description": "Deal details — present on content_type=sale cards only.", "properties": { "value": { "type": "number", "nullable": true }, "source": { "type": "string", "nullable": true }, "close_date": { "type": "string", "format": "date-time", "nullable": true }, "loss_reason": { "type": "string", "nullable": true } } }, "assignee": { "type": "object", "nullable": true, "properties": { "name": { "type": "string", "nullable": true }, "email": { "type": "string", "nullable": true } } }, "first_thread": { "type": "object", "nullable": true, "properties": { "title": { "type": "string", "nullable": true }, "received_at": { "type": "string", "format": "date-time", "nullable": true }, "from": { "type": "string", "nullable": true } } } } } ``` ### CreateTaskRequest ```json { "type": "object", "required": [ "list_id", "title" ], "properties": { "list_id": { "type": "string", "description": "Opaque list id (lst_…) to create the task in." }, "title": { "type": "string", "description": "Short task title shown on the card." }, "notes": { "type": "string", "description": "Optional free-text body/notes for the task." }, "due_at": { "type": "string", "format": "date-time", "description": "Optional due date/time, ISO-8601 (e.g. 2026-06-15T09:00:00Z)." } } } ``` ### MoveTaskRequest ```json { "type": "object", "required": [ "list_id" ], "properties": { "list_id": { "type": "string", "description": "Destination opaque list id (lst_…)." } } } ``` ### UpdateTaskRequest ```json { "type": "object", "description": "Partial update — send only the fields to change. At least one is required.", "properties": { "shade": { "type": "string", "enum": [ "none", "yellow", "cyan", "light_blue", "teal", "blue", "indigo", "yellow_green", "lime", "green", "amber", "red", "magenta", "purple", "orange", "deep_orange", "pink", "brown", "grey", "dark_grey", "black" ], "description": "The task's colour; 'none' clears it." }, "custom_fields": { "type": "array", "description": "Custom-field values to set. Each needs field_id (from GET /boards/{id}/custom-fields) and value (option id(s) for tag/status; raw value otherwise).", "items": { "$ref": "#/components/schemas/CustomFieldValue" } } } } ``` ### CreateBoardRequest ```json { "type": "object", "required": [ "team_id", "name" ], "properties": { "team_id": { "type": "string", "description": "Team/organisation id (from /me or /teams) to create the shared board under." }, "name": { "type": "string", "description": "Board name." } } } ``` ### UpdateBoardRequest ```json { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "description": "New board name." } } } ``` ### CreateListRequest ```json { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "description": "List name." } } } ``` ### UpdateListRequest ```json { "type": "object", "required": [ "name" ], "properties": { "name": { "type": "string", "description": "New list name." } } } ``` ### CreateCustomFieldRequest ```json { "type": "object", "required": [ "name", "type" ], "properties": { "name": { "type": "string", "description": "Field name." }, "type": { "type": "string", "enum": [ "text", "multiline_text", "number", "date", "checkbox", "url", "phone", "select", "status", "tag", "multiselect" ], "description": "Field type. Option types (select/status/tag/multiselect) accept an `options` list." }, "options": { "type": "array", "nullable": true, "description": "For option-type fields: the selectable options. Each is created with a fresh id.", "items": { "type": "object", "required": [ "label" ], "properties": { "label": { "type": "string" } } } }, "display_on_card": { "type": "boolean", "description": "Show this field on task cards. Only valid for `status` and `tag` fields — sending it for any other type returns a validation error." } } } ``` ### UpdateCustomFieldRequest ```json { "type": "object", "description": "Partial update — send only what changes. At least one of name, options or display_on_card is required.", "properties": { "name": { "type": "string", "description": "New field name." }, "options": { "type": "array", "nullable": true, "description": "Replaces the field options. Include an option `id` to keep an existing option; omit it to add a new one.", "items": { "type": "object", "required": [ "label" ], "properties": { "id": { "type": "string" }, "label": { "type": "string" } } } }, "display_on_card": { "type": "boolean", "description": "Show this field on task cards. Only valid for `status` and `tag` fields — sending it for any other type returns a validation error." } } } ``` ### EmailAddress ```json { "type": "object", "properties": { "email": { "type": "string" }, "name": { "type": "string", "nullable": true } } } ``` ### EmailMessage ```json { "type": "object", "description": "A message on an email thread. Ids are raw Gmail message ids (hex). Bodies are present only when the thread was fetched with include=messages (email.body.read scope).", "properties": { "id": { "type": "string", "description": "Raw Gmail message id (hex)." }, "subject": { "type": "string", "nullable": true }, "from": { "$ref": "#/components/schemas/EmailAddress" }, "to": { "type": "array", "items": { "$ref": "#/components/schemas/EmailAddress" } }, "cc": { "type": "array", "items": { "$ref": "#/components/schemas/EmailAddress" } }, "date": { "type": "string", "format": "date-time", "nullable": true }, "snippet": { "type": "string", "nullable": true }, "unread": { "type": "boolean" }, "labels": { "type": "array", "items": { "type": "string" }, "description": "Raw Gmail label ids on the message." }, "body_text": { "type": "string", "nullable": true, "description": "Plain-text body (include=messages only)." }, "body_html": { "type": "string", "nullable": true, "description": "HTML body (include=messages only)." } } } ``` ### EmailThread ```json { "type": "object", "description": "A thread in the key owner's mailbox. Thread/message ids are the RAW Gmail ids (hex) — pass them to other Gmail tooling as-is.", "properties": { "provider": { "type": "string", "description": "Email provider, e.g. gmail." }, "thread_id": { "type": "string", "description": "Raw Gmail thread id (hex)." }, "message_ids": { "type": "array", "items": { "type": "string" } }, "subject": { "type": "string", "nullable": true }, "participants": { "type": "array", "items": { "$ref": "#/components/schemas/EmailAddress" } }, "latest_message_at": { "type": "string", "format": "date-time", "nullable": true }, "snippet": { "type": "string", "nullable": true, "description": "Snippet of the latest message." }, "direction": { "type": "string", "nullable": true, "enum": [ "inbound", "outbound", null ], "description": "inbound = latest message received, outbound = latest message sent by the mailbox owner." }, "unread": { "type": "boolean", "description": "True when any message on the thread is unread." }, "linked_tasks": { "type": "array", "nullable": true, "description": "Sortd tasks this thread is attached to (that the key can access). Present on the single-thread GET; omitted from search results — fetch the thread to see its tasks.", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Opaque task id (tsk_…)." }, "title": { "type": "string", "nullable": true } } } }, "messages": { "type": "array", "nullable": true, "items": { "$ref": "#/components/schemas/EmailMessage" }, "description": "Present only when include=messages." } } } ``` ### Contact ```json { "type": "object", "description": "A CRM contact. Search returns the company-grouped subset; get/create/update return the full record.", "properties": { "id": { "type": "string", "description": "Opaque contact id (con_…)." }, "name": { "type": "string", "nullable": true, "description": "Contact display name." }, "first_name": { "type": "string", "nullable": true }, "last_name": { "type": "string", "nullable": true }, "title": { "type": "string", "nullable": true }, "type": { "type": "string", "nullable": true, "description": "Contact type, e.g. sale / general / hire." }, "stage": { "type": "string", "nullable": true, "description": "Pipeline stage, e.g. Lead / Customer." }, "source": { "type": "string", "nullable": true }, "contact_info": { "type": "array", "description": "Emails, phones, etc.", "items": { "type": "object", "properties": { "type": { "type": "string" }, "value": { "type": "string" } } } }, "tags": { "type": "array", "items": { "type": "string" } }, "company": { "type": "object", "nullable": true, "properties": { "id": { "type": "string", "nullable": true, "description": "Opaque company id (cmp_…)." }, "name": { "type": "string", "nullable": true } } } } } ``` ### Company ```json { "type": "object", "description": "A CRM company.", "properties": { "id": { "type": "string", "description": "Opaque company id (cmp_…)." }, "name": { "type": "string", "nullable": true }, "type": { "type": "string", "nullable": true }, "stage": { "type": "string", "nullable": true }, "company_info": { "type": "array", "description": "Website, domain, phone, etc.", "items": { "type": "object", "properties": { "type": { "type": "string" }, "value": { "type": "string" } } } }, "tags": { "type": "array", "items": { "type": "string" } } } } ``` ### CreateContactRequest ```json { "type": "object", "required": [ "team_id", "name" ], "description": "Create a CRM contact. Link a company via company_id (existing cmp_) or company_name (find-or-create); if neither, the org default company is used.", "properties": { "team_id": { "type": "string", "description": "Team/organisation id (required)." }, "name": { "type": "string", "description": "Display name (required)." }, "first_name": { "type": "string", "nullable": true }, "last_name": { "type": "string", "nullable": true }, "title": { "type": "string", "nullable": true }, "type": { "type": "string", "nullable": true, "description": "Default general." }, "stage": { "type": "string", "nullable": true }, "source": { "type": "string", "nullable": true }, "contact_info": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string" }, "value": { "type": "string" } } } }, "tags": { "type": "array", "items": { "type": "string" } }, "company_id": { "type": "string", "nullable": true, "description": "Existing company (cmp_…) to link." }, "company_name": { "type": "string", "nullable": true, "description": "Company name to find-or-create (ignored if company_id is given)." } } } ``` ### UpdateContactRequest ```json { "type": "object", "description": "Partial update — provide only the fields to change.", "properties": { "name": { "type": "string", "nullable": true }, "first_name": { "type": "string", "nullable": true }, "last_name": { "type": "string", "nullable": true }, "title": { "type": "string", "nullable": true }, "type": { "type": "string", "nullable": true }, "stage": { "type": "string", "nullable": true }, "source": { "type": "string", "nullable": true }, "contact_info": { "type": "array", "nullable": true, "items": { "type": "object", "properties": { "type": { "type": "string" }, "value": { "type": "string" } } } }, "tags": { "type": "array", "nullable": true, "items": { "type": "string" } }, "company_id": { "type": "string", "nullable": true, "description": "Re-link to a different existing company (cmp_…)." } } } ``` ### CreateCompanyRequest ```json { "type": "object", "required": [ "team_id" ], "properties": { "team_id": { "type": "string", "description": "Team/organisation id (required)." }, "name": { "type": "string", "nullable": true }, "type": { "type": "string", "nullable": true }, "stage": { "type": "string", "nullable": true }, "company_info": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string" }, "value": { "type": "string" } } } }, "tags": { "type": "array", "items": { "type": "string" } } } } ``` ### UpdateCompanyRequest ```json { "type": "object", "description": "Partial update — provide only the fields to change.", "properties": { "name": { "type": "string", "nullable": true }, "type": { "type": "string", "nullable": true }, "stage": { "type": "string", "nullable": true }, "company_info": { "type": "array", "nullable": true, "items": { "type": "object", "properties": { "type": { "type": "string" }, "value": { "type": "string" } } } }, "tags": { "type": "array", "nullable": true, "items": { "type": "string" } } } } ``` ### EmailLabel ```json { "type": "object", "description": "A Gmail label/folder in the mailbox.", "properties": { "id": { "type": "string", "description": "Gmail label id — system labels are fixed (INBOX, SENT, SPAM, TRASH, …); user labels are Label_… " }, "name": { "type": "string", "nullable": true, "description": "Display name." }, "type": { "type": "string", "enum": [ "system", "user" ], "description": "system = Gmail built-in (INBOX/SENT/SPAM/TRASH/DRAFT/STARRED/IMPORTANT/CATEGORY_*); user = custom label." } } } ``` ### EmailLabels ```json { "type": "object", "description": "The mailbox's labels.", "properties": { "labels": { "type": "array", "items": { "$ref": "#/components/schemas/EmailLabel" } } } } ``` ### Draft ```json { "type": "object", "description": "A Gmail draft (D47: drafts live only in Gmail — the user can edit, send or delete it there; there is no approval state). Ids are raw Gmail ids (hex), except draft_id which is Gmail’s opaque draft id.", "properties": { "draft_id": { "type": "string", "description": "Gmail draft id — pass to the send-draft endpoint." }, "message_id": { "type": "string", "nullable": true, "description": "Raw Gmail message id (hex) of the draft message." }, "thread_id": { "type": "string", "nullable": true, "description": "Raw Gmail thread id (hex)." }, "subject": { "type": "string", "nullable": true }, "snippet": { "type": "string", "nullable": true }, "to": { "type": "array", "items": { "type": "string" } }, "cc": { "type": "array", "items": { "type": "string" } }, "date_modified": { "type": "string", "format": "date-time", "nullable": true } } } ``` ### CreateDraftRequest ```json { "type": "object", "required": [ "body" ], "properties": { "to": { "type": "array", "nullable": true, "items": { "type": "string" }, "description": "Recipient email addresses (optional — a draft can be recipient-less until the human fills it in)." }, "cc": { "type": "array", "nullable": true, "items": { "type": "string" } }, "bcc": { "type": "array", "nullable": true, "items": { "type": "string" } }, "subject": { "type": "string", "nullable": true, "description": "Defaults to \"Re: \"." }, "body": { "type": "string", "description": "The draft body." }, "is_html": { "type": "boolean", "description": "Whether body is HTML (default true)." } } } ``` ### CreateNewDraftRequest ```json { "type": "object", "required": [ "to", "subject", "body" ], "description": "A brand-new draft (no thread). Unlike the thread-scoped draft, recipients and subject are required.", "properties": { "to": { "type": "array", "items": { "type": "string" }, "description": "Recipient email addresses (at least one)." }, "cc": { "type": "array", "nullable": true, "items": { "type": "string" } }, "bcc": { "type": "array", "nullable": true, "items": { "type": "string" } }, "subject": { "type": "string", "description": "The draft subject." }, "body": { "type": "string", "description": "The draft body. May be an empty string (e.g. an attachment-only draft)." }, "is_html": { "type": "boolean", "description": "Whether body is HTML (default true)." } } } ``` ### ReplyRequest ```json { "type": "object", "required": [ "body" ], "description": "Recipients CANNOT be set — they are derived from the thread (D49). Requests containing to/cc/bcc are rejected.", "properties": { "body": { "type": "string", "description": "The reply body." }, "is_html": { "type": "boolean", "description": "Whether body is HTML (default true)." }, "mode": { "type": "string", "enum": [ "reply", "reply_all" ], "description": "reply = sender only (honouring Reply-To); reply_all = keep the To/Cc audience. Default reply." } } } ``` ### SendEmailRequest ```json { "type": "object", "required": [ "to", "subject", "body" ], "properties": { "to": { "type": "array", "items": { "type": "string" }, "description": "Recipient email addresses." }, "cc": { "type": "array", "nullable": true, "items": { "type": "string" } }, "bcc": { "type": "array", "nullable": true, "items": { "type": "string" } }, "subject": { "type": "string" }, "body": { "type": "string" }, "is_html": { "type": "boolean", "description": "Whether body is HTML (default true)." }, "thread_id": { "type": "string", "nullable": true, "description": "Optional raw Gmail thread id (hex) to send onto an existing thread." } } } ``` ### UpdateWebhookRequest ```json { "type": "object", "description": "Provide at least one field. `status` toggles delivery: `disabled` pauses, `active` resumes (and clears the failure streak).", "properties": { "events": { "type": "array", "nullable": true, "items": { "type": "string" }, "description": "Replace the subscribed event types (at least one valid event)." }, "target_url": { "type": "string", "nullable": true, "description": "Replace the delivery URL." }, "status": { "type": "string", "enum": [ "active", "disabled" ], "description": "active = deliver; disabled = pause (events still recorded)." }, "filters": { "$ref": "#/components/schemas/WebhookFilters" } } } ``` ### WebhookFilters ```json { "type": "object", "nullable": true, "description": "Optional per-subscription delivery filters. `scope` applies to `thread.reply_received`: `tasked` (default) delivers only replies on threads you have an accessible linked task for; `all` delivers every inbound message. Send `null` to clear.", "properties": { "scope": { "type": "string", "enum": [ "tasked", "all" ], "description": "thread.reply_received delivery scope. Default tasked." } } } ``` ### CreateWebhookRequest ```json { "type": "object", "required": [ "target_url", "events" ], "description": "Subscribe a target url to one or more event types. The response includes the signing secret once.", "properties": { "target_url": { "type": "string", "description": "HTTPS endpoint that receives signed event deliveries." }, "events": { "type": "array", "items": { "type": "string" }, "description": "Event types to subscribe to (at least one valid event)." }, "filters": { "$ref": "#/components/schemas/WebhookFilters" } } } ``` ### ReadStateRequest ```json { "type": "object", "required": [ "read" ], "properties": { "read": { "type": "boolean", "description": "true = mark read, false = mark unread." } } } ``` ### LabelsRequest ```json { "type": "object", "description": "At least one of add/remove is required.", "properties": { "add": { "type": "array", "nullable": true, "items": { "type": "string" }, "description": "Raw Gmail label ids to add." }, "remove": { "type": "array", "nullable": true, "items": { "type": "string" }, "description": "Raw Gmail label ids to remove." } } } ``` ### SnoozeRequest ```json { "type": "object", "required": [ "until" ], "properties": { "until": { "type": "string", "format": "date-time", "description": "When the thread should return to the Inbox. Must be in the future." } } } ``` ### Mailbox ```json { "type": "object", "description": "The key owner's mailbox connection state. A v2 key reaches only its owner's own mailbox (Flow A).", "properties": { "connected": { "type": "boolean", "description": "True when a mailbox is connected and its stored credentials are usable." }, "provider_id": { "type": "string", "nullable": true, "description": "Email provider id, e.g. itemprovider-gmail. Null when not connected." }, "email_address": { "type": "string", "nullable": true, "description": "The mailbox address. Null when not connected." }, "reason": { "type": "string", "nullable": true, "enum": [ null, "not_connected", "reauth_required" ], "description": "Why connected is false: not_connected = no email account linked; reauth_required = the stored authorisation is no longer usable and the user must reconnect their mailbox in Sortd." } } } ``` ### ErrorEnvelope ```json { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "request_id": { "type": "string" }, "details": {} } } } } ```