Skip to main content
Help centre

Taskshire API and webhooks

The complete reference, with every endpoint, filter, field and error code, plus signature examples in PHP, JavaScript and Python.

Who this is for: Developers and technical teammates who connect Taskshire to other systems. New here? Start with The Taskshire API.

The REST API lets other tools read and change tasks in the projects you work on. Outgoing webhooks tell your systems when something happens.

  • Base URL: https://{your Taskshire host}/api/v1
  • Format: JSON in, JSON out. Send Accept: application/json.
  • Times: ISO 8601. Responses are always in UTC (2026-10-05T14:30:00Z).

Authentication

Create a personal access token on your Profile page, under API tokens:

  • Give it a name and choose Read only or Read and write.
  • Choose when it expires: 30 days, 90 days, 6 months or a year. Every token expires, and a year is the longest you can choose. Create a new token before the old one runs out; an expired token gets 401 unauthenticated, just like a revoked one.
  • Confirm your password. Creating and revoking tokens is recorded in your account's activity.
  • The token is shown once. Store it somewhere safe.
  • You can hold up to 20 tokens at once.

Send the token with every request:

Authorization: Bearer 12|tsk_…

A token acts as you. It has your roles on every project you belong to, and every change goes through the same permission checks as the app. For example, if your role can't reassign tasks, the API can't either.

Ability Allows
read Every GET endpoint
write Every other method (POST, PATCH, DELETE)

Some rules to know:

  • A browser session never authenticates the API. Only tokens do.
  • Tokens prefixed tsk_ can be found by secret scanners.
  • Revoking a token on the profile page stops it straight away.
  • Last used shows when a token last made a request.

Rate limit

Each person can make 120 requests a minute. The limit is per person, not per token: two tokens owned by the same person share the same 120 requests, and replays of idempotent writes count too. Above that you get 429 with a Retry-After header.

Conventions

Resources

Each resource has this shape:

{
  "type": "task",
  "id": "9b2f…",
  "attributes": { "…": "…" },
  "links": { "self": "…", "web": "…" }
}
  • A single resource comes back as {"data": {…}}.
  • A list comes back as {"data": […], "links": {…}, "meta": {…}}.

Errors

Every error has the same shape:

{
  "error": {
    "code": "validation_failed",
    "message": "Give the ticket a title of up to 255 characters.",
    "details": { "fields": { "title": ["Give the ticket a title of up to 255 characters."] } }
  }
}
Status code When
401 unauthenticated Missing, wrong or expired token
403 forbidden The token lacks the ability, or your role can't do this
404 not_found The item doesn't exist, or you can't see it (the API never confirms either)
409 stale_caption Someone saved the caption since you read it
409 idempotency_in_progress A request with the same Idempotency-Key is still running
412 precondition_failed If-Match didn't match the task's current ETag
422 validation_failed, unknown_fields, transition_refused, caption_too_long, invalid_sort The request can't be applied
422 idempotency_key_reused, invalid_idempotency_key The Idempotency-Key was used with a different body, or is longer than 255 characters
423 content_locked The copy is locked in this stage, or the channel has published
429 rate_limited Too many requests

Pagination

Task and time-entry lists use cursors:

  • Set the page size with per_page (1 to 100, default 25).
  • Follow links.next until it's null.
  • Cursors keep working while tasks are added, so you won't skip or repeat a task.

The pages list (GET /projects/{project}/docs) uses plain page numbers instead: per_page (1 to 100, default 50) and page, with meta.total and meta.has_more telling you whether to ask for the next page.

Retrying writes safely

Networks fail. To retry a POST, PATCH, PUT or DELETE without doing it twice, send an Idempotency-Key header: any string of 1 to 255 characters that is unique to that one operation (a UUID is ideal).

  • The first request runs as normal. Its response (any status below 500) is kept for 24 hours.
  • A later request with the same token, method, path and key gets that stored response back, with the header Idempotent-Replayed: true. Nothing runs again.
  • The same key with a different body is refused with 422 idempotency_key_reused. Use a fresh key for each new operation.
  • If the first request is still running, a duplicate gets 409 idempotency_in_progress with Retry-After: 2.
  • Keys are scoped to the token that sent them. GET requests ignore the header.
  • Responses of 500 and above aren't kept, so a retry after a server error runs the request again.
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1d2c8e-5b1a-4f0e-9c3d-2a7b8e9f0a11" \
  https://app.example.com/api/v1/projects/dark-queen/tasks -d '{"title": "Launch trailer"}'

Tasks by id or reference

Wherever a path has {task}, you can use the task's id, its reference (DQS-142) or just its number (142). Wherever a path has {project}, you can use the project's id or its slug.


Endpoints

You and your projects

GET /me
GET /projects?include_archived=1
GET /projects/{project}
GET /projects/{project}/stages
GET /projects/{project}/members
GET /projects/{project}/labels
  • /projects lists projects where you hold an internal role. Client reviewers use their approval links instead.
  • /members returns each accepted member with their roles.

Tasks

GET    /projects/{project}/tasks
POST   /projects/{project}/tasks
GET    /projects/{project}/tasks/{task}
PATCH  /projects/{project}/tasks/{task}
DELETE /projects/{project}/tasks/{task}
POST   /projects/{project}/tasks/{task}/archive
POST   /projects/{project}/tasks/{task}/restore
POST   /projects/{project}/tasks/{task}/move

List filters

All filters are optional and can be combined.

Parameter Example Meaning
stage drafting,internal_review Stage ids or slugs
assignee me, none, 42 Who it's assigned to
label Launch or a label id Labels, by id or name (has any of them)
priority high,urgent none, low, medium, high or urgent
due_after, due_before 2026-11-01 A date or ISO time. Dates are in your timezone; due_before includes the whole day.
updated_since 2026-10-05T09:00:00Z Changed since this time (useful for syncing)
q trailer or DQS-142 Title contains the text, or matches the reference
archived exclude (default), include, only Whether to include archived tasks
sort -created_at (default), created_at, -updated_at, updated_at Order. A leading - means newest first.
curl -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
  "https://app.example.com/api/v1/projects/dark-queen/tasks?stage=internal_review&assignee=me&per_page=50"

Show

The full task includes:

  • the brief, as HTML and as plain text;
  • watchers;
  • channels and their captions, with caption versions;
  • the checklist and its progress;
  • custom fields.

Responses carry an ETag. Send it back as If-None-Match to get 304 Not Modified when nothing has changed.

Create

curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  https://app.example.com/api/v1/projects/dark-queen/tasks -d '{
    "title": "Launch trailer",
    "brief": "<p>Cut a 30-second trailer.</p>",
    "assignee_id": 42,
    "priority": "high",
    "due_at": "2026-11-01",
    "label_ids": ["…"],
    "channel_ids": ["…"],
    "checklist": ["Storyboard", "Edit"],
    "custom_values": {"<field id>": 1200}
  }'
  • New tasks start in the first stage.
  • The response is 201 Created, with a Location header.
  • Accepted fields: title (required), brief, assignee_id, campaign_id, scheduled_for, due_at, start_at, priority, channel_ids, label_ids, watcher_ids, checklist, custom_values.
  • Unknown fields are refused with unknown_fields.

Update

Send only the fields you want to change:

  • title, brief, priority, due_at, start_at, campaign_id, label_ids (replaces the labels) need the draft permission;
  • assignee_id needs the assign or reassign permission;
  • scheduled_for needs the reschedule permission.

To make an update conditional, send the ETag you read as If-Match.

curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H 'If-Match: "3f1c…"' \
  https://app.example.com/api/v1/projects/dark-queen/tasks/DQS-142 -d '{"priority": "urgent"}'

Move

POST /projects/{project}/tasks/{task}/move
{ "stage": "internal_review", "comment": "Ready for a look", "publish_now": false }
  • stage is a stage id or slug.
  • The workflow decides whether the move is allowed, just as on the board.
  • Some moves need a comment, such as requesting revisions or rejecting.
  • A refusal returns 422 transition_refused with the reason and the two stages:
{"error": {"code": "transition_refused", "message": "Cannot move from Internal review to Scheduled.",
  "details": {"from_stage": {"slug": "internal_review", …}, "to_stage": {"slug": "scheduled", …}}}}

Archive, restore and delete

  • archive and restore need the cancel or configure permission.
  • DELETE moves the task to the bin. A project admin can restore it in the app.

Comments

GET  /projects/{project}/tasks/{task}/comments
POST /projects/{project}/tasks/{task}/comments
  • The list shows internal comments only to roles that can read them. Other roles see only comments shared with the client.
  • Post a comment with {"body": "…", "visibility": "internal" | "shared", "parent_id": "…"}.
  • A reply takes its thread's visibility.
  • To mention someone, write @[Name](user:42).

Captions

GET   /projects/{project}/tasks/{task}/captions
PATCH /projects/{project}/tasks/{task}/captions/{caption}
{ "caption": "New copy", "expected_version": 3 }

Captions use optimistic locking. Send the version you read as expected_version.

  • If someone saved since, you get 409 stale_caption. details holds current_caption, current_version and edited_by.
  • Captions over the platform's limit get 422 caption_too_long.
  • Locked copy gets 423 content_locked.

Checklist

GET    /projects/{project}/tasks/{task}/checklist
POST   /projects/{project}/tasks/{task}/checklist          {"body": "…", "assignee_id": 42, "due_at": "2026-11-01"}
PATCH  /projects/{project}/tasks/{task}/checklist/{item}   {"body": "…", "done": true}
DELETE /projects/{project}/tasks/{task}/checklist/{item}

Subtasks and dependencies

Task objects include parent_id, subtask_counts ({done, total}), blocked and estimate_minutes.

GET    /projects/{project}/tasks/{task}/subtasks
POST   /projects/{project}/tasks/{task}/subtasks           same body as creating a task, without board_id
GET    /projects/{project}/tasks/{task}/dependencies
POST   /projects/{project}/tasks/{task}/dependencies       {"blocker_id": "<task id or reference such as DQS-142>"}
DELETE /projects/{project}/tasks/{task}/dependencies/{blocker}

A subtask lives on its parent's board and starts in that board's first stage. Subtasks are one level deep. Loops, self-links and more than 20 blockers return 422 invalid_dependency; a parent that is itself a subtask returns 422 structure_refused.

Time tracking

People see and change their own time; project admins see everyone's, plus rate and amount (minor units).

Method Path Ability Notes
GET /projects/{project}/time-entries read Filters: from, to (YYYY-MM-DD), user_id (admins), task (id or reference), label_id, campaign_id, billable (yes/no), per_page (max 100). Cursor-paginated, newest first.
POST /projects/{project}/time-entries write started_at (ISO 8601), minutes or duration ("1h 30m"), optional task, note, billable, confirm_overlap. 201.
PATCH /projects/{project}/time-entries/{entry} write Any of task, started_at, ended_at, minutes/duration, note, billable, confirm_overlap.
DELETE /projects/{project}/time-entries/{entry} write 204.
GET /timer read Your running timer, or data: null.
POST /projects/{project}/timer/start write Optional task, note. Stops any other timer you have; returns data and stopped. 201.
POST /timer/stop write 409 no_timer_running if none.
GET /projects/{project}/time-summary read Same filters, plus group_by (person, task, label, campaign, week). Returns totals, groups, budget (admins).

Errors: 409 overlapping_time (resend with "confirm_overlap": true), 403 forbidden for locked entries you cannot change.

Pages and project overview

Method Path Ability Notes
GET /projects/{project}/docs read q search; per_page (1–100, default 50), page; returns data[], meta{total,per_page,page,has_more}
GET /projects/{project}/docs/{doc} read body_html, body_text, lock_version; ?format=md returns text/markdown
POST /projects/{project}/docs write title, icon, parent_id, template, body (HTML, sanitised). 201.
PATCH /projects/{project}/docs/{doc} write title, icon, body, lock_version; a stale lock_version returns 409 conflict
GET /projects/{project}/overview read overview, goals, client_contacts, links
PATCH /projects/{project}/overview write Project admins. Max 10 contacts, 12 links, https links only.

Webhooks

Project admins add webhooks in Project tools → Webhooks (also linked from Project settings → General and from Integrations).

Events

Event Sent when
task.created A task is created: by hand, by a request form, by the API or by an automation
task.updated Title, brief, assignee, dates, labels, priority or campaign change. data.changes holds {field: {from, to}}.
task.moved A task changes stage. Includes data.from_stage and data.to_stage.
task.commented A comment is posted. Includes data.comment, with its visibility.
task.published A task reaches a published stage
task.deleted A task is deleted
task.archived A task is archived; data.actor_id says who.
task.restored A task comes back from the archive or the bin; data.from is archive or bin.
webhook.test You press Send test

Request

Each delivery is a POST with a JSON body:

{
  "id": "0b6c…",
  "event": "task.moved",
  "created_at": "2026-10-05T14:30:00Z",
  "project": {"id": "…", "slug": "dark-queen", "name": "Dark Queen"},
  "data": {
    "task": {"type": "task", "id": "…", "attributes": {"reference": "DQS-142", "title": "…", "stage": {…}, …}, "links": {…}},
    "from_stage": {"id": "…", "slug": "drafting", "name": "Work In Progress"},
    "to_stage": {"id": "…", "slug": "internal_review", "name": "Internal review"},
    "actor_id": 42
  }
}

It comes with these headers:

Content-Type: application/json
User-Agent: Taskshire-Webhooks/1.0
X-Taskshire-Event: task.moved
X-Taskshire-Delivery: 0b6c…           (same as body.id; use it to drop duplicates)
X-Taskshire-Signature: t=1760000000,v1=5d0a…

Verifying the signature

v1 is the hex HMAC-SHA256 of "{t}.{raw request body}", keyed with the endpoint's signing secret (whsec_…). To check it:

  1. Recompute v1 over the raw body.
  2. Compare the two values in constant time.
  3. Reject any request where t is more than five minutes from now.
[$t, $v1] = sscanf($_SERVER['HTTP_X_TASKSHIRE_SIGNATURE'], 't=%d,v1=%s');
$expected = hash_hmac('sha256', $t.'.'.file_get_contents('php://input'), $secret);
$valid = hash_equals($expected, $v1) && abs(time() - $t) <= 300;
const [, t, v1] = req.headers['x-taskshire-signature'].match(/t=(\d+),v1=([a-f0-9]+)/);
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) && Math.abs(Date.now() / 1000 - t) <= 300;
t, v1 = (p.split("=", 1)[1] for p in header.split(","))
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
valid = hmac.compare_digest(expected, v1) and abs(time.time() - int(t)) <= 300

You can create a new secret at any time with New secret. The old one stops signing straight away.

Delivery, retries and switching off

  • Any 2xx response counts as delivered. Redirects aren't followed.
  • Each request waits up to 10 seconds for a response.
  • Failed deliveries are retried after 30 seconds, then 1, 2, 4, 8, 16 and 32 minutes, up to 8 attempts in total.
  • After 20 failed attempts in a row, the webhook is switched off. Project admins get a notice in their inbox, and by email if they get emails.
  • Turn the webhook back on in settings once the endpoint is fixed.
  • The delivery log keeps 30 days of deliveries. For each one it shows the payload, the response code and an excerpt of the response. Redeliver sends a delivery again as a new delivery, with redelivery_of set.

Allowed addresses

Webhook addresses must be public https:// URLs on port 443 or a port above 1023. They can't contain a username or password.

Addresses that resolve to loopback, private, link-local (including cloud metadata), carrier-grade NAT or reserved ranges are refused. This is checked:

  • when you save the webhook;
  • again before every delivery, with the connection pinned to the address that passed the check.

Automations and request forms

These have no API endpoints yet. Tasks they create or change still show up through the task endpoints and webhooks:

  • Automations act as the project admin who last saved them.
  • Request forms create tasks as the form's owner.

← All guides