Tasks live in a list, on a board, in a team. This walks that hierarchy and creates a task two ways: a plain task, and a task created from an email thread — the API equivalent of dragging the email onto a board.
A plain task needs a key with tasks.write (plus boards.read / lists.read to discover ids). Creating from an email additionally needs email.metadata.read — the thread is read to build the card. See Authenticate first.
curl "https://api.sortd.com/v2/teams" \
-H "Authorization: Bearer sk_live_…"
# -> { "data": [ { "id": "<team_id>", "name": "Acme" } ], "meta": { … } }
curl "https://api.sortd.com/v2/boards?team_id=<team_id>" \
-H "Authorization: Bearer sk_live_…"
# -> { "data": [ { "id": "brd_…", "name": "Support", "board_type": "shared" } ], … }
curl "https://api.sortd.com/v2/boards/brd_…/lists" \
-H "Authorization: Bearer sk_live_…"
# -> { "data": [ { "id": "lst_…", "name": "To do", "board_id": "brd_…" } ], … }
Send list_id and title (required), plus optional notes and due_at. Include an Idempotency-Key so a retried request can't create a duplicate.
curl -X POST "https://api.sortd.com/v2/tasks" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <unique-key>" \
-d '{
"list_id": "lst_…",
"title": "Follow up with Acme re: renewal",
"notes": "Sent pricing on Tuesday",
"due_at": "2026-06-15T09:00:00Z"
}'
const res = await fetch("https://api.sortd.com/v2/tasks", {
method: "POST",
headers: {
Authorization: "Bearer sk_live_…",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
list_id: "lst_…",
title: "Follow up with Acme re: renewal",
notes: "Sent pricing on Tuesday",
}),
});
const { data } = await res.json();
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "sk_live_…");
client.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());
var content = new StringContent("""
{ "list_id": "lst_…", "title": "Follow up with Acme re: renewal" }
""", Encoding.UTF8, "application/json");
var res = await client.PostAsync("https://api.sortd.com/v2/tasks", content);
var json = await res.Content.ReadAsStringAsync();
import requests, uuid
res = requests.post(
"https://api.sortd.com/v2/tasks",
headers={
"Authorization": "Bearer sk_live_…",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"list_id": "lst_…", "title": "Follow up with Acme re: renewal"},
)
task = res.json()["data"]
Send thread_id — the raw Gmail thread id (hex) — instead of composing the card yourself. The task's title defaults to the email subject (send title only to override it), the thread stays linked to the task so replies keep landing on it, and notes / due_at work exactly as in 4a. Optional archive: true also archives the email in Gmail once it's on the board — mirroring the drag gesture.
Needs tasks.write and email.metadata.read. Get thread ids from GET /email-threads?q=… (a raw Gmail search) or from webhook payloads.
curl -X POST "https://api.sortd.com/v2/tasks" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <unique-key>" \
-d '{
"list_id": "lst_…",
"thread_id": "18c2a5b7f3d94e01",
"due_at": "2026-06-15T09:00:00Z",
"archive": true
}'
const search = await fetch(
"https://api.sortd.com/v2/email-threads?q=" + encodeURIComponent("from:jane@acme.com renewal"),
{ headers: { Authorization: "Bearer sk_live_…" } }
);
const thread = (await search.json()).data[0];
const res = await fetch("https://api.sortd.com/v2/tasks", {
method: "POST",
headers: {
Authorization: "Bearer sk_live_…",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ list_id: "lst_…", thread_id: thread.thread_id }),
});
const { data } = await res.json(); // data.title === the email subject
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", "sk_live_…");
client.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());
var content = new StringContent("""
{ "list_id": "lst_…", "thread_id": "18c2a5b7f3d94e01", "archive": true }
""", Encoding.UTF8, "application/json");
var res = await client.PostAsync("https://api.sortd.com/v2/tasks", content);
import requests, uuid
res = requests.post(
"https://api.sortd.com/v2/tasks",
headers={
"Authorization": "Bearer sk_live_…",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"list_id": "lst_…", "thread_id": "18c2a5b7f3d94e01"},
)
task = res.json()["data"] # task["title"] == the email subject
By default a new task goes to the top of the list — the same as an automation-created card. Both forms of create accept:
| Field | Effect |
|---|---|
"position": "top" | Top of the list (the default — you can omit it). |
"position": "bottom" | Bottom of the list. Use this when creating several tasks in sequence — with the default, each new card lands above the last and the batch comes out in reverse order. |
"after_task_id": "tsk_…" | Directly below that task. Takes precedence over position. |
# curl — a batch that keeps its order
curl -X POST "https://api.sortd.com/v2/tasks" \
-H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
-d '{ "list_id": "lst_…", "title": "Step 1", "position": "bottom" }'
curl -X POST "https://api.sortd.com/v2/tasks" \
-H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
-d '{ "list_id": "lst_…", "title": "Step 2", "position": "bottom" }'
# curl — slot a task directly below another
curl -X POST "https://api.sortd.com/v2/tasks" \
-H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
-d '{ "list_id": "lst_…", "title": "Chase invoice", "after_task_id": "tsk_…" }'
Repositioning tasks that already exist — including a full AI re-prioritisation — is its own guide: Order & reorder tasks.
A task can carry several threads. POST /tasks/{id}/threads links another one (needs tasks.write + email.metadata.read). Linking a thread that's already on the task is a no-op, so retries are safe. The response returns the task's full thread_ids.
curl -X POST "https://api.sortd.com/v2/tasks/tsk_…/threads" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{ "thread_id": "18c2b90a44e7d2af", "archive": true }'
# -> { "data": { "id": "tsk_…", "thread_ids": ["18c2a5b7f3d94e01", "18c2b90a44e7d2af"] } }
{
"data": {
"id": "tsk_…",
"title": "Q3 renewal",
"list_id": "lst_…",
"completed": false,
"due_at": "2026-06-15T09:00:00Z",
"created_at": "2026-06-09T12:00:00Z",
"thread_ids": ["18c2a5b7f3d94e01"]
},
"meta": { "request_id": "req_…" }
}
thread_ids holds every linked thread — pass them to the email endpoints (GET /email-threads/{id}, reply, drafts). Mark the task done with POST /tasks/{id}/complete, or move it with POST /tasks/{id}/move.
PATCH /tasks/{id} is a partial update — send only what changes. It supports the task colour (the shade field) as a named value; "none" clears it.
shade: red · orange · deep_orange · amber · yellow · yellow_green · lime · green · teal · cyan · light_blue · blue · indigo · purple · magenta · pink · brown · grey · dark_grey · black · none
curl -X PATCH "https://api.sortd.com/v2/tasks/tsk_…" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{ "shade": "red" }'
await fetch("https://api.sortd.com/v2/tasks/tsk_…", {
method: "PATCH",
headers: { Authorization: "Bearer sk_live_…", "Content-Type": "application/json" },
body: JSON.stringify({ shade: "none" }),
});
import requests
requests.patch(
"https://api.sortd.com/v2/tasks/tsk_…",
headers={"Authorization": "Bearer sk_live_…"},
json={"shade": "lime"},
)
The response is the full updated task, with shade as a colour name.
Tags and status are custom fields, set through the same PATCH. First read the board's fields to get each field_id and, for tag/status/select fields, the option ids:
curl "https://api.sortd.com/v2/boards/brd_…/custom-fields" \
-H "Authorization: Bearer sk_live_…"
# -> { "data": [
# { "id": "cf_status", "name": "Status", "type": "status",
# "options": [ { "id": "opt_open", "label": "Open" }, { "id": "opt_done", "label": "Done" } ] },
# { "id": "cf_priority", "name": "Priority", "type": "tag",
# "options": [ { "id": "opt_hi", "label": "High" }, { "id": "opt_lo", "label": "Low" } ] }
# ], … }
Then set values by field_id. For tag/status/select, value is an array of option ids ([] clears it); for text/number/date it's the raw value. Needs the customfields.write scope.
Fields flagged "ai": true (managed by Sortd AI, e.g. Response Status) are read-only — setting them returns a 400.
curl -X PATCH "https://api.sortd.com/v2/tasks/tsk_…" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"custom_fields": [
{ "field_id": "cf_status", "value": ["opt_open"] },
{ "field_id": "cf_priority", "value": ["opt_hi"] }
]
}'
await fetch("https://api.sortd.com/v2/tasks/tsk_…", {
method: "PATCH",
headers: { Authorization: "Bearer sk_live_…", "Content-Type": "application/json" },
body: JSON.stringify({ custom_fields: [{ field_id: "cf_ref", value: "ACME-1234" }] }),
});
import requests
requests.patch(
"https://api.sortd.com/v2/tasks/tsk_…",
headers={"Authorization": "Bearer sk_live_…"},
json={
"shade": "red",
"custom_fields": [{"field_id": "cf_status", "value": ["opt_done"]}],
},
)
The updated task's custom_fields holds the stored values — option ids for tag/status; resolve their labels via the board's custom-fields endpoint above.
Next: order & reorder tasks, or classify a thread with AI.