Developers
API, webhooks and integrations
Read and change projects, tasks and dependencies from your own code, and get a signed webhook as soon as something changes. The same API powers our Zapier, Make and n8n integrations.
The technical reference below is in English.
Quick start
Create a personal API key in Profile → API keys (read-only or read + write), then call the REST API with it as a bearer token. Base URL: https://planitude.app.
curl https://planitude.app/api/projects \
-H "Authorization: Bearer $PLANITUDE_KEY"curl -X POST https://planitude.app/api/projects/$PROJECT_ID/tasks \
-H "Authorization: Bearer $PLANITUDE_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Foundations", "durationMin": 1440, "externalId": "crm-4711"}'curl "https://planitude.app/api/projects/$PROJECT_ID/tasks?externalId=crm-4711" \
-H "Authorization: Bearer $PLANITUDE_KEY"Durations and lags are in working minutes (an 8-hour day is 480). Instants are ISO 8601 in UTC. Errors are JSON: {"error": "<code>", "details": {...}}.
Authentication
API keys (gnt_…) are personal: they act as you, with your roles on each project, and never more. Choose read or read + write; you can restrict a key to one workspace and give it an expiry. Every call is logged and visible next to the key.
OAuth 2.1 is available for apps that connect on behalf of users (this is what the Zapier app uses): authorization code with PKCE (S256), refresh tokens rotated on every use. Pass resource=https://planitude.app/api to the authorize endpoint to get a token for the REST API; scopes are planitude:read and planitude:write. Clients register with POST /api/oauth/register (RFC 7591). Endpoints: https://planitude.app/oauth/authorize, https://planitude.app/api/oauth/token, https://planitude.app/api/oauth/revoke; metadata at https://planitude.app/.well-known/oauth-authorization-server.
For safety, keys and OAuth apps cannot delete projects or tasks: deleting stays in the web app, where a person confirms it.
Rate limits
Per key, in a sliding window of one minute: 240 read requests and 60 write requests. Over the limit you get 429 rate_limited with a Retry-After header (seconds). Other limits: 50 webhooks per workspace, 1,000 changes per change set.
Tasks, search and pagination
GET /api/projects/{id}/tasks is paginated with a cursor: pass limit (1-500, default 100) and the nextCursor of the previous page as cursor. Filters: externalId (exact), q (name contains), updatedSince, completed=true|false; order with sort=number (default), -number (newest first) or -updatedAt.
Write with POST /api/projects/{id}/tasks (one task, or a change set applied in a single transaction), PATCH /api/projects/{id}/tasks/{taskId}, PUT …/tasks/{taskId}/assignees with project members from GET …/members, and POST …/dependencies (FS, SS, FF, SF, with lag; a dependency that would create a cycle is refused with 409). New projects: POST /api/projects or POST /api/projects/from-template with a template from GET /api/templates.
Webhooks
Webhooks send an HTTPS POST with a JSON body to your endpoint whenever something changes, whether the change comes from the web app, the API, the MCP server or an import. Create them in Profile → Webhooks or with the API (REST hooks: subscribe with POST /api/webhooks, unsubscribe with DELETE /api/webhooks/{id}). A webhook covers one project, or every project of a workspace (owners and admins only).
curl -X POST https://planitude.app/api/webhooks \
-H "Authorization: Bearer $PLANITUDE_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/planitude", "events": ["task.created", "task.completed"], "projectId": "'$PROJECT_ID'"}'
# -> 201 {"webhook": {...}, "secret": "whsec_..."} (the secret is shown only once)| Event | When |
|---|---|
project.created | A project was created: blank, from a template, duplicated or imported (data.source). |
project.updated | Project fields changed, or a saved version was restored (data.changes, data.reason). |
project.deleted | A project was deleted. |
task.created | A task was created. |
task.updated | Task fields or assignees changed (data.changes has only what changed). |
task.deleted | A task was deleted (one event per task when a branch is deleted). |
task.completed | Task progress reached 100%. |
task.dates_changed | Start or finish moved: by an edit (cause: edit) or by the recalculation (cause: reschedule). |
dependency.created | A dependency was created or its type/lag changed. |
dependency.deleted | A dependency was removed. |
baseline.saved | A baseline was saved (manually or automatically at the first progress). |
member.invited | An invite link was created for the project. |
member.joined | A person joined the project. |
comment.created | A comment was added to a task. |
Every request carries Planitude-Event, Planitude-Event-Id, Planitude-Delivery and Planitude-Signature. The body is versioned (apiVersion: "2026-10-01"): fields may be added, never removed or retyped without a new version.
{
"id": "6f1c1d1e-2f4b-4d0a-9a77-2a3c5b1e9f10",
"type": "task.completed",
"apiVersion": "2026-10-01",
"createdAt": "2026-10-01T08:30:12.418Z",
"workspaceId": "824650d2-d0b3-4cac-8df8-5743ae9ff65a",
"projectId": "40be00e0-7e34-4f7a-864b-5cc986f575b8",
"actor": {
"id": "b1666432-132a-4ea9-b1ae-76ea69e226c5",
"name": "Anna Rossi"
},
"data": {
"task": {
"id": "4dbd104a-00cd-4035-8955-255f52ea586a",
"key": "WHD-1",
"number": 1,
"projectId": "40be00e0-7e34-4f7a-864b-5cc986f575b8",
"parentId": null,
"name": "Excavation",
"type": "task",
"startAt": "2026-10-05T06:00:00.000Z",
"finishAt": "2026-10-08T15:00:00.000Z",
"durationMin": 1920,
"progressPct": 100,
"completed": true,
"isCritical": true,
"externalId": "ext-001",
"assignees": [
{
"resourceId": "0c6a…",
"userId": "b166…",
"name": "Anna Rossi",
"unitsPct": 100
}
],
"url": "https://planitude.app/app/t/WHD-1",
"…": "all other task fields"
}
}
}Verifying signatures
The header looks like Planitude-Signature: t=1727771412,v1=5257a869…. v1 is the hex HMAC-SHA256 of <t>.<raw body> with your signing secret. Compute it on the raw body (before parsing JSON), compare in constant time, and reject requests whose t is more than 5 minutes away from your clock: a captured request cannot be replayed later.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyPlanitude(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // replay
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}import hashlib, hmac, time
def verify_planitude(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False # replay
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Rotate the secret any time from the profile or with POST /api/webhooks/{id}/rotate-secret; the old one stops working immediately.
Retries and delivery
Answer with any 2xx within 10 seconds. Redirects are not followed. On errors and timeouts Planitude retries up to 6 times in total: immediately, then after 1 min, 5 min, 30 min, 2 h, 12 h. After 5 events in a row fail every attempt, the webhook is disabled and its owner receives an email; turn it back on from the profile. A 410 Gone answer disables it at once (REST hook convention).
Delivery is at least once and not ordered: use the event id (Planitude-Event-Id) to ignore duplicates and createdAt to order. The delivery log (status, HTTP code, response excerpt, payload, manual resend) is kept for 30 days. Only public addresses are accepted as destinations: private, loopback, link-local and cloud-metadata addresses are refused, also after DNS resolution.
Zapier, Make and n8n
Zapier: triggers New Task, Task Completed, Task Dates Changed and New Project (instant, with REST hooks), actions Create Task, Update Task, Create Project from Template and Find Task. Make: custom app with the same modules and instant triggers. n8n: community node n8n-nodes-planitude with a trigger node and task/project operations. All three use this API and these webhooks: anything they do, you can do with a script.
API reference
The complete reference (every endpoint, field and webhook payload) is the OpenAPI 3.1 document at /api/openapi.json: import it into Postman, Insomnia or a code generator. The event catalog is also available at /api/webhooks/events.