Create a task — plain, or from an email

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.

1. Find your team

curl "https://api.sortd.com/v2/teams" \
  -H "Authorization: Bearer sk_live_…"
# -> { "data": [ { "id": "<team_id>", "name": "Acme" } ], "meta": { … } }

2. Pick a board

curl "https://api.sortd.com/v2/boards?team_id=<team_id>" \
  -H "Authorization: Bearer sk_live_…"
# -> { "data": [ { "id": "brd_…", "name": "Support", "board_type": "shared" } ], … }

3. Pick a list on that board

curl "https://api.sortd.com/v2/boards/brd_…/lists" \
  -H "Authorization: Bearer sk_live_…"
# -> { "data": [ { "id": "lst_…", "name": "To do", "board_id": "brd_…" } ], … }

4a. Create a plain task

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

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"
  }'

JavaScript (fetch)

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();

C# (HttpClient)

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();

Python (requests)

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"]

4b. Create a task from an email

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 — title comes from the email subject

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
  }'

JavaScript — find the thread, then task it

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

C# (HttpClient)

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);

Python (requests)

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

5. Choose where the card lands

By default a new task goes to the top of the list — the same as an automation-created card. Both forms of create accept:

FieldEffect
"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"] } }

7. The created task

{
  "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.

8. Edit the task

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 — set the task colour to red

curl -X PATCH "https://api.sortd.com/v2/tasks/tsk_…" \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "shade": "red" }'

JavaScript — clear the colour

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" }),
});

Python — set the colour to lime

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.

9. Set tags, status & other custom fields

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. Each value is typed by the property's type, the same shape you get back when you read the task; null clears any of them. Needs the customfields.write scope. If any value in the request is invalid, nothing is changed.

TypeWriteRead back
text, url, phone, email, locationa string (URLs must be http(s); email is checked)the string
multiline_texta string with line breaksthe same string
numbera number (or a numeric string)a number
dateYYYY-MM-DD (shown as that day in every time zone) or an ISO date-timean ISO date-time
checkboxtrue / falsetrue / false, plus checked_at
select, statusone option id or label{ "id", "label" } or null
multiselect, taga list of option ids or labelsa list of { "id", "label" }
personthe email of a member of the task's board{ "email", "name" }
contact / companya con_… / cmp_… id from the task's organisation{ "id", "name" }
attachmentthe full list: { "id" } keeps one, { "url", "name" } adds a linka list of { "id", "name", "url", "mime_type", "provider" }
tasks (sub-tasks)use the sub-task endpoints belowa list of { "id", "title", "completed", "completed_at", "assignee", "position" }

Fields flagged "ai": true (managed by Sortd AI, e.g. Response Status) take values the same way, and behave as in the app: setting Response Status to Actioned clears the AI fields rather than storing Actioned. Their settings (name, options, display) cannot be changed and they cannot be deleted — those requests return a 400.

curl — set Status = Open and Priority = High

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": ["High"] }
    ]
  }'

JavaScript — set a text custom field

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" }] }),
});

Python — change a colour and a custom field in one call

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": "Done"}],
    },
)

The updated task's custom_fields lists every property with its type, name and typed value, for example:

"custom_fields": [
  { "field_id": "cf_status", "type": "status", "name": "Status", "value": { "id": "opt_done", "label": "Done" } },
  { "field_id": "cf_owner", "type": "person", "name": "Owner", "value": { "email": "sam@acme.com", "name": "Sam" } },
  { "field_id": "cf_ok", "type": "checkbox", "name": "Approved", "value": true, "checked_at": "2026-10-02T09:14:00.000Z" }
]

Sub-tasks

A Task list property (type tasks) holds sub-tasks. Change them one at a time — add, then tick, rename, assign (a board member's email) or move (position), or delete. Each change is recorded in the task history as in the app. Needs tasks.write and customfields.write.

# add
curl -X POST "https://api.sortd.com/v2/tasks/tsk_…/custom-fields/cf_checklist/subtasks" \
  -H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
  -d '{ "title": "Send the signed contract", "assignee": "sam@acme.com" }'
# tick it (subtask id from the response or the task's custom_fields)
curl -X PATCH "https://api.sortd.com/v2/tasks/tsk_…/custom-fields/cf_checklist/subtasks/<subtask_id>" \
  -H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
  -d '{ "completed": true }'

Next: order & reorder tasks, or classify a thread with AI.