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
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/me | The key's workspace and scopes. |
| GET | /api/v1/projects | Active projects (add ?archived=true for all). |
| GET | /api/v1/projects/{key} | One project. |
| GET | /api/v1/projects/{key}/statuses | The project's workflow, in order. |
| GET | /api/v1/projects/{key}/fields | Custom fields and their options. |
| GET | /api/v1/projects/{key}/tasks | Tasks. 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}/sprints | Sprints (projects that use them) with dates, status, task and estimate totals, and completion snapshots. |
| POST | /api/v1/projects/{key}/sprints | Plan a sprint: { "name", "goal", "start_date", "end_date" } (all optional; defaults follow the last sprint). |
| POST | /api/v1/sprints/{id}/start | Start a planned sprint (one active sprint per project). |
| POST | /api/v1/sprints/{id}/complete | Complete the active sprint: { "move_open_tasks_to": "next" | "new" | "backlog" }. |
| GET | /api/v1/projects/{key}/updates | Status updates for a project, newest first (health, summary, body, author). |
| POST | /api/v1/projects/{key}/updates | Post a status update: { "health": "on_track" | "at_risk" | "off_track" | "done", "summary", "body" }. |
| GET | /api/v1/projects/{key}/forms | Request 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}/submissions | Public: submit { "answers": { "<question id>": value } }. Creates a task; returns its reference. 422 with per-field errors, 429 when rate limited. |
| GET | /cal/{token}.ics | Public: 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/portfolio | Every active project with health, latest update, owner, target date, progress, overdue and goals (Business). |
| GET | /api/v1/goals | Goals with computed progress and status, linked projects and sub-goals (Business). |
| POST | /api/v1/goals/{id}/check-ins | Record progress: { "value", "note" } (the key result's current value, or a % for manual goals). |
| GET | /api/v1/projects/{key}/views | Saved views you can see in a project (yours and shared ones), with their filters and a summary. |
| POST | /api/v1/projects/{key}/tasks | Create 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}/subtasks | A task's subtasks (one level), in order. |
| POST | /api/v1/tasks/{taskKey}/subtasks | Add 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}/approvals | Approval state and rounds (approvers by email, status, note). Task JSON also carries "approval": none | pending | approved | changes_requested. |
| POST | /api/v1/tasks/{taskKey}/approvals | Ask for approval: { "approvers": [email], "message" }. Replaces an open round. Free: 1 approver, 5 open requests. |
| POST | /api/v1/approvals/{id}/decision | As the approver: { "decision": "approved" | "changes_requested", "note" } (a note is required for changes). Fires the task.approval_decided webhook. |
| PATCH | /api/v1/tasks/bulk | Change 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-delete | Move up to 200 tasks to the trash: { "keys": [...] }. Returns { trashed, skipped }. |
| POST | /api/v1/tasks/{taskKey}/restore | Restore 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}/trash | Tasks in the project's trash, newest first, with deleted_at, deleted_by and purge_in_days. |
| GET | /api/v1/tasks/{taskKey}/followers | People following a task (name, email, why they follow). |
| POST | /api/v1/tasks/{taskKey}/followers | Follow the task yourself, or add someone: { "email": "…" } (needs edit access). |
| DELETE | /api/v1/tasks/{taskKey}/followers | Unfollow yourself, or ?email=… to remove someone (needs edit access). |
| GET | /api/v1/tasks/{taskKey}/comments | Comments, oldest first. |
| POST | /api/v1/tasks/{taskKey}/comments | Add a comment: { "body": "…" }. |
| GET | /api/v1/tasks/{taskKey}/time | Time entries on a task. |
| POST | /api/v1/tasks/{taskKey}/time | Log time: { "minutes": 90, "date": "2026-10-01", "note": "…" } (or "duration": "1h 30m"). |
| GET | /api/v1/time | Time in a range (Pro): from, to (YYYY-MM-DD), user (email), project (key). Owners and admins see everyone. |
| GET | /api/v1/members | Workspace members (name, email, role). |
| GET | /api/v1/invoices | Invoices (owner/admin keys): number, status, amounts in cents including tax, period and links. |
| GET | /api/v1/templates | Built-in and saved project templates. |
| GET | /api/v1/projects/{key}/meetings | Meetings in a project, newest first. |
| POST | /api/v1/projects/{key}/meetings | Create 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-items | Add an action item: { "title", "assignee": email, "due_date", "create_task": true }. |
| POST | /api/v1/projects | Create a project: { "name": "…", "key": "OPS", "template": "hiring", "start_date": "2026-11-02" }. |
| GET | /api/v1/me/tasks | The 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.