Skip to content

Developers

API & webhooks

Create and update tasks from your other tools, and get a signed POST whenever work changes. Available on Pro (3 keys, 3 webhooks) and Business (unlimited).

Authentication

Owners and admins create keys in Settings → Developers. A key is shown once; store it as a secret. Keys are read-only unless you allow writes, and they act as the person who created them (their role applies, and the key stops working if they leave the workspace).

curl https://your-torvany.example/api/v1/me \
  -H "Authorization: Bearer pw_live_xxxxxxxx_…"

Endpoints

MethodPathDescription
GET/api/v1/meThe key's workspace and scopes.
GET/api/v1/projectsActive projects (add ?archived=true for all).
GET/api/v1/projects/{key}One project.
GET/api/v1/projects/{key}/statusesThe project's workflow, in order.
GET/api/v1/projects/{key}/fieldsCustom fields and their options.
GET/api/v1/projects/{key}/tasksTasks. Filters: status, assignee (email or none), updated_since, q, view (a saved view id), sprint (current, backlog or a sprint id). Paginate with limit (≤ 100) and cursor.
GET/api/v1/projects/{key}/sprintsSprints (projects that use them) with dates, status, task and estimate totals, and completion snapshots.
POST/api/v1/projects/{key}/sprintsPlan a sprint: { "name", "goal", "start_date", "end_date" } (all optional; defaults follow the last sprint).
POST/api/v1/sprints/{id}/startStart a planned sprint (one active sprint per project).
POST/api/v1/sprints/{id}/completeComplete the active sprint: { "move_open_tasks_to": "next" | "new" | "backlog" }.
GET/api/v1/projects/{key}/updatesStatus updates for a project, newest first (health, summary, body, author).
POST/api/v1/projects/{key}/updatesPost a status update: { "health": "on_track" | "at_risk" | "off_track" | "done", "summary", "body" }.
GET/api/v1/projects/{key}/formsRequest forms in a project with their questions, public token and submission counts.
GET/api/v1/forms/{token}Public: an enabled public form's questions (id, label, type, required, options). No API key needed.
POST/api/v1/forms/{token}/submissionsPublic: submit { "answers": { "<question id>": value } }. Creates a task; returns its reference. 422 with per-field errors, 429 when rate limited.
GET/cal/{token}.icsPublic: an iCalendar feed (RFC 5545) of your open tasks with due dates and your meetings, or one project's. Create the token with Subscribe in the app; it acts as the credential.
GET/api/v1/portfolioEvery active project with health, latest update, owner, target date, progress, overdue and goals (Business).
GET/api/v1/goalsGoals with computed progress and status, linked projects and sub-goals (Business).
POST/api/v1/goals/{id}/check-insRecord progress: { "value", "note" } (the key result's current value, or a % for manual goals).
GET/api/v1/projects/{key}/viewsSaved views you can see in a project (yours and shared ones), with their filters and a summary.
POST/api/v1/projects/{key}/tasksCreate a task.
GET/api/v1/tasks/{taskKey}One task, e.g. WEB-12.
PATCH/api/v1/tasks/{taskKey}Update any of the task's fields, including sprint_id (a sprint id, or null for the backlog).
DELETE/api/v1/tasks/{taskKey}Move a task to the trash (restorable for 30 days). ?permanent=true (admins) deletes it forever, also from the trash.
GET/api/v1/tasks/{taskKey}/subtasksA task's subtasks (one level), in order.
POST/api/v1/tasks/{taskKey}/subtasksAdd a subtask: { "title", "assignee": email, "due_date" }. Up to 50 per task. Set or clear a parent with PATCH /tasks/{key} { "parent": "WEB-1" | null }.
GET/api/v1/tasks/{taskKey}/approvalsApproval state and rounds (approvers by email, status, note). Task JSON also carries "approval": none | pending | approved | changes_requested.
POST/api/v1/tasks/{taskKey}/approvalsAsk for approval: { "approvers": [email], "message" }. Replaces an open round. Free: 1 approver, 5 open requests.
POST/api/v1/approvals/{id}/decisionAs the approver: { "decision": "approved" | "changes_requested", "note" } (a note is required for changes). Fires the task.approval_decided webhook.
PATCH/api/v1/tasks/bulkChange up to 200 tasks: { "keys": ["WEB-1", …], "set": { "status", "assignee": email, "priority", "due_date", "due_shift_days": 7, "sprint": id|null, "fields": { "Team": "Design" } } }. Names resolve per project; returns { updated, skipped: [{ key, reason }] }.
POST/api/v1/tasks/bulk-deleteMove up to 200 tasks to the trash: { "keys": [...] }. Returns { trashed, skipped }.
POST/api/v1/tasks/{taskKey}/restoreRestore a task from the trash. Returns the task; a finished sprint becomes the backlog and a departed assignee becomes unassigned.
GET/api/v1/projects/{key}/trashTasks in the project's trash, newest first, with deleted_at, deleted_by and purge_in_days.
GET/api/v1/tasks/{taskKey}/followersPeople following a task (name, email, why they follow).
POST/api/v1/tasks/{taskKey}/followersFollow the task yourself, or add someone: { "email": "…" } (needs edit access).
DELETE/api/v1/tasks/{taskKey}/followersUnfollow yourself, or ?email=… to remove someone (needs edit access).
GET/api/v1/tasks/{taskKey}/commentsComments, oldest first.
POST/api/v1/tasks/{taskKey}/commentsAdd a comment: { "body": "…" }.
GET/api/v1/tasks/{taskKey}/timeTime entries on a task.
POST/api/v1/tasks/{taskKey}/timeLog time: { "minutes": 90, "date": "2026-10-01", "note": "…" } (or "duration": "1h 30m").
GET/api/v1/timeTime in a range (Pro): from, to (YYYY-MM-DD), user (email), project (key). Owners and admins see everyone.
GET/api/v1/membersWorkspace members (name, email, role).
GET/api/v1/invoicesInvoices (owner/admin keys): number, status, amounts in cents including tax, period and links.
GET/api/v1/templatesBuilt-in and saved project templates.
GET/api/v1/projects/{key}/meetingsMeetings in a project, newest first.
POST/api/v1/projects/{key}/meetingsCreate a meeting: { "title", "starts_at", "attendees": [emails], "agenda" }.
GET/api/v1/meetings/{id}A meeting with attendees, notes, action items and linked tasks.
PATCH/api/v1/meetings/{id}Update title, starts_at, agenda or notes.
POST/api/v1/meetings/{id}/action-itemsAdd an action item: { "title", "assignee": email, "due_date", "create_task": true }.
POST/api/v1/projectsCreate a project: { "name": "…", "key": "OPS", "template": "hiring", "start_date": "2026-11-02" }.
GET/api/v1/me/tasksThe key owner's work: status=open|done, mode=assigned|created. Open items include their group (overdue, today, week, later, none).

Working with tasks

Refer to people by email, statuses by name, and custom fields by their name and option labels. Dates are YYYY-MM-DD. Send an Idempotency-Key header on POST to make retries safe (replays return the first response for 24 hours).

curl -X POST https://your-torvany.example/api/v1/projects/WEB/tasks \
  -H "Authorization: Bearer $TORVANY_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: deal-4821" \
  -d '{
    "title": "Kick-off with Acme",
    "status": "To do",
    "assignee": "[email protected]",
    "priority": "high",
    "due_date": "2026-11-03",
    "fields": { "Type": "Feature" }
  }'

Responses wrap data: lists return { "data": [...], "next_cursor": "…" | null }; single items return { "data": { … } }.

Recurring tasks

Send recurrence on create or PATCH to make a task repeat, or null to stop. When it's completed, the next occurrence is created (you'll get task.created) and the rule moves to it. freq is daily, weekly, monthly or yearly; weekly rules list weekdays (0 = Sunday); basis is schedule (from the due date) or completion; ends is never, on a date, or after a number of times. Tasks return the rule with a readable summary, plus series.

curl -X PATCH https://your-torvany.example/api/v1/tasks/OPS-12 \
  -H "Authorization: Bearer $TORVANY_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "recurrence": { "freq": "weekly", "interval": 1, "weekdays": [1], "ends": { "type": "after", "count": 10 } } }'

Webhooks

Add an HTTPS endpoint in Settings → Developers and choose events. We POST JSON within seconds and retry failures after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. After 15 failures in a row the endpoint is disabled and admins are emailed. Respond with any 2xx within 10 seconds.

  • task.createdA task was created (in the app, by import or through the API).
  • task.updatedFields changed. data.changed lists them (title, status, assignee, …).
  • task.completedA task entered a Done status (also sent as task.updated).
  • task.deletedA task was moved to the trash. data.task is the last snapshot.
  • task.restoredA task came back from the trash. data.task is the restored task.
  • comment.createdA comment was added. Mentions appear as @Name.
  • project.createdA project was created.
  • project.archivedA project was archived.
  • form.submittedSomeone submitted a request form. data has form, task (id, key, title) and submitter (email, name).
{
  "id": "evt_8f3a…",
  "type": "task.updated",
  "created_at": "2026-10-02T09:30:00.000Z",
  "workspace": { "id": "…", "slug": "acme-inc", "name": "Acme Inc" },
  "data": { "task": { "key": "WEB-12", "title": "…", "status": "In progress", … }, "changed": ["status"] }
}

Verifying signatures

Each request has Torvany-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of t + "." + raw body with your endpoint's secret. Reject requests older than 5 minutes. Torvany-Event and Torvany-Delivery headers identify the event and attempt; use the event id to ignore duplicates.

// Node.js
import crypto from "node:crypto";
function verify(secret, rawBody, header) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
# Python
import hmac, hashlib, time
def verify(secret: str, raw_body: bytes, header: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), f'{parts["t"]}.'.encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Errors & limits

Errors look like { "error": { "code": "validation_failed", "message": "…", "field": "status" } }. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.

  • 400 bad_json / bad_requestThe body isn't valid JSON, or a parameter is malformed.
  • 401 unauthorizedMissing, invalid, revoked or expired key, or its owner left the workspace.
  • 402 plan_required / plan_limitThe plan doesn't include the API, or a plan limit was reached.
  • 403 insufficient_scope / forbiddenA read-only key tried to write, or the key's owner lacks permission.
  • 404 not_foundNo such project or task in this workspace.
  • 409 idempotency_conflictAn Idempotency-Key was reused with a different request.
  • 422 validation_failedInvalid input; error.field says which.
  • 429 rate_limitedMore than 120 requests a minute for this key. See Retry-After.