Skip to content

API Reference

The TruePPM REST API is documented via OpenAPI 3.0.3, auto-generated by drf-spectacular.

FormatURL
Swagger UIhttp://localhost:8000/api/schema/swagger-ui/
Raw YAMLhttp://localhost:8000/api/schema/
http://localhost:8000/api/v1/

TruePPM uses JWT auth with a split-token model: the short-lived access token is returned in the JSON body and held in memory by the client, while the long-lived refresh token is delivered in an httpOnly, Secure, SameSite=Strict cookie that JavaScript can never read. This protects the high-value refresh credential from theft via XSS — an injected script can ride the current session but cannot exfiltrate the refresh token.

Because the refresh token lives in a cookie, browser clients must send credentials on the auth requests (fetch(..., { credentials: "include" }) or xhr.withCredentials = true).

POST /api/v1/auth/token/
Content-Type: application/json
{"username": "...", "password": "..."}

Returns only the access token in the body:

{"access": "<jwt>"}

The refresh token is set in a response cookie (default name trueppm_refresh), scoped to Path=/api/v1/auth/token/refresh/ so it is sent only on the refresh request and never on ordinary API calls:

Set-Cookie: trueppm_refresh=<jwt>; HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth/token/refresh/

Pass the access token on all subsequent requests:

Authorization: Bearer <access_token>
POST /api/v1/auth/token/refresh/

The refresh endpoint reads the refresh token from the cookie — it is no longer accepted in the request body, so the request has no body. It returns a new access token:

{"access": "<jwt>"}

A request that arrives without a valid refresh cookie returns 401. When refresh rotation is enabled the cookie is re-issued (rotated) on each successful refresh; the previous token is blacklisted if the blacklist app is installed.

The TokenRefresh / TokenRefreshRequest body schemas are intentionally gone from the OpenAPI document — the refresh endpoint takes no request body.

POST /api/v1/auth/logout/

Clears the refresh cookie and best-effort blacklists the presented refresh token (when the blacklist app is installed). Idempotent — always returns 205 Reset Content, whether or not a cookie was present.

The legacy bare AUTH_REFRESH_COOKIE_* names are still accepted as fallbacks.

SettingDefaultPurpose
TRUEPPM_AUTH_REFRESH_COOKIE_NAMEtrueppm_refreshCookie name for the refresh token.
TRUEPPM_AUTH_REFRESH_COOKIE_PATH/api/v1/auth/token/refresh/Restricts the cookie to the refresh endpoint.
TRUEPPM_AUTH_REFRESH_COOKIE_SAMESITEStrictCSRF posture — the cookie is never sent cross-site.
TRUEPPM_AUTH_REFRESH_COOKIE_SECURETrueHTTPS-only cookie. Set False only for non-TLS local development.

Project-scoped API token (projectApiTokenAuth)

Section titled “Project-scoped API token (projectApiTokenAuth)”

The inbound task-sync surface uses a separate, non-JWT scheme. Mint a token in Project settings → API tokens; it is scoped to a single project and authorizes only the task-sync endpoint (ADR-0068). Send it as a bearer token:

POST /api/v1/projects/{project_id}/task-sync/
Authorization: Bearer tppm_<64-hex>

The schema advertises this scheme as projectApiTokenAuth. It is deliberately not interchangeable with the JWT session — a logged-in user cannot call task-sync with their normal credentials, so every inbound push is attributable to a minted token. A token whose project does not match the URL returns 401 (not 403) so callers cannot enumerate project existence.

MethodPathDescription
GET/api/v1/calendars/List
POST/api/v1/calendars/Create
GET/api/v1/calendars/{id}/Retrieve
PUT / PATCH/api/v1/calendars/{id}/Update
DELETE/api/v1/calendars/{id}/Soft-delete
MethodPathDescription
GET/api/v1/projects/List (scoped to your memberships)
POST/api/v1/projects/Create (caller becomes Owner)
GET/api/v1/projects/{id}/Retrieve
PUT / PATCH/api/v1/projects/{id}/Update
DELETE/api/v1/projects/{id}/Soft-delete

Projects and programs carry the inheritable sharing settings. The override fields public_sharing and allow_guests are nullable (null = inherit from the parent scope) and writable by an Owner/Admin; the resolved fields effective_public_sharing, inherited_public_sharing, effective_allow_guests, and inherited_allow_guests are read-only. See Sharing & Access Inheritance.

Projects and programs also carry the inheritable attachment policy (the same Workspace → Program → Project chain). The override fields are writable by an Owner/Admin:

FieldTypeMeaning
attachments_enabledboolean | nullWhether file uploads are permitted. null = inherit from the parent scope.
allowed_attachment_typesstring[] | nullMIME allow-list (tri-state): null = inherit, [] = explicitly allow nothing, [...] = an explicit set.

The resolved fields effective_attachments_enabled, inherited_attachments_enabled, effective_allowed_attachment_types, and inherited_allowed_attachment_types are read-only. effective_* is the value in force after inheritance; inherited_* is what the parent scope would supply (what effective_* falls back to when the override is null).

Writing a MIME type that is permanently security-denied (text/html, image/svg+xml, application/xhtml+xml) into allowed_attachment_types returns 400 — these can never be allowed, at any scope. An empty list is accepted. See Task collaboration for how the resolved policy governs uploads.

MethodPathDescription
GET/api/v1/projects/{id}/members/List (Viewer+)
POST/api/v1/projects/{id}/members/Add member (Owner only)
GET/api/v1/projects/{id}/members/{mid}/Retrieve
PATCH/api/v1/projects/{id}/members/{mid}/Change role (Owner only)
DELETE/api/v1/projects/{id}/members/{mid}/Remove (Owner, or self)

See RBAC for the permission matrix and role escalation rules.

A program is a container for related projects (see Programs).

MethodPathDescription
GET/api/v1/programs/List (scoped to your memberships)
POST/api/v1/programs/Create (caller becomes Owner)
GET/api/v1/programs/{id}/Retrieve
PUT / PATCH/api/v1/programs/{id}/Update
DELETE/api/v1/programs/{id}/Soft-delete
GET/api/v1/programs/samples/List the bundled samples available to the demo loader
POST/api/v1/programs/load-sample/Load a bundled sample program (the in-app “Load demo data” action); body {"sample": "<key>"}
POST/api/v1/programs/import/Import a JSON seed document as a new program (raw JSON body or multipart file upload); caller becomes Owner
GET/api/v1/programs/{id}/export/Download the program as a canonical JSON seed file (Content-Disposition: attachment)
GET/api/v1/programs/{id}/rollup-config/Read the program rollup KPIs config (enabled KPIs + aggregation policy)
PATCH/api/v1/programs/{id}/rollup-config/Update the program rollup KPIs config (Admin only)
GET/api/v1/programs/{id}/risk-policy/Read the program risk & dependencies policy
PATCH/api/v1/programs/{id}/risk-policy/Update the program risk & dependencies policy (Admin only)
POST/api/v1/programs/bulk-fields/Bulk-set inherited settings (methodology, iteration label, risk policy) across multiple programs; body {"ids": [...], "fields": {...}} — only the named rows and fields change (Workspace Admin)
POST/api/v1/programs/{id}/bulk-project-fields/Bulk-set inherited settings (methodology, iteration label) across this program’s projects; body {"ids": [...], "fields": {...}} (Program Admin)
GET/api/v1/programs/{id}/resource-contention/Within-program resource contention across member projects (Scheduler+; optional ?start= / ?end= window, repeatable ?resource= / ?status=)
GET/api/v1/programs/{id}/schedule/Program-true cross-project critical path — merges every member project’s tasks and every accepted cross-project dependency into one CPM run, computed on read. Tasks in projects you cannot read are redacted to a minimal card (title + forecast dates only); links are flagged cross-project (any program member)
POST/api/v1/programs/{id}/split/Split a program into sub-programs — planned, not yet implemented (returns 501)

The import endpoint returns 201 Created with the new program; it returns 400 with an errors array on a malformed or oversized seed document. The load-sample endpoint returns 201 Created with a {program, landing_project_id, sample_key} envelope — landing_project_id is the project board to land a contributor on so their assigned work is visible (null when the sample has no open sprint), and sample_key echoes the loaded sample. See Sample projects.

The rollup-config and risk-policy endpoints use a method-level permission split: GET is open to any program member (closed programs remain readable for audit), while PATCH requires the Admin role and is blocked on closed programs. Both are partial updates — send only the fields you want to change — and every successful PATCH is audited automatically.

resource-contention returns each resource with their task spans across every member project of the program, each span tagged with its source project, so the client can surface people over-allocated across sibling projects in overlapping windows. Overallocation detection is intentionally client-side. The window defaults to the earliest start and latest finish across member projects; it returns 409 if no member project has a computed schedule yet, and 400 for an invalid date or a start after end. This is within-program visibility only — cross-program leveling and the portfolio heat map remain Enterprise.

Program split is a planned endpoint that validates the request payload and the caller’s Owner role, then returns 501 Not Implemented with a detail message and a tracking_issue number. The request contract it accepts is {"splits": [{"name": str, "project_ids": [uuid]}, ...]}; the working implementation is not yet available.

MethodPathDescription
GET/api/v1/tasks/List (filter: ?project=, ?is_critical=true)
GET/api/v1/tasks/search/Board card search (required: ?project=, ?q=); returns slim {id, name, status, short_id} matches
POST/api/v1/tasks/Create
GET/api/v1/tasks/{id}/Retrieve
PUT / PATCH/api/v1/tasks/{id}/Update
DELETE/api/v1/tasks/{id}/Soft-delete (cascades to edges)

CPM fields (early_start, early_finish, late_start, late_finish, total_float, is_critical) are read-only — set by the auto-scheduler.

Each attachment is either an uploaded file or an external URL — never both.

MethodPathDescription
GET/api/v1/projects/{id}/tasks/{task_id}/attachments/List (Viewer+)
POST/api/v1/projects/{id}/tasks/{task_id}/attachments/Add (Member+); multipart file xor external_url
GET/api/v1/projects/{id}/tasks/{task_id}/attachments/{att_id}/Retrieve (Viewer+)
DELETE/api/v1/projects/{id}/tasks/{task_id}/attachments/{att_id}/Soft-delete (uploader or Admin+)
GET/api/v1/projects/{id}/tasks/{task_id}/attachments/{att_id}/signed-url/Issue a short-lived download URL (file attachments only)

File uploads are governed by the project’s resolved attachment policy (effective_attachments_enabled / effective_allowed_attachment_types on the project — see Projects):

  • If the resolved attachments_enabled is false, a file upload returns 403. External-URL attachments are not affected by attachments_enabled.
  • The uploaded file’s MIME type must be in the project’s resolved allow-list (not a fixed list). A disallowed type returns 415 with code attachment_unsupported_mime. The declared MIME is also content-sniffed against the real bytes, so a payload that masquerades as an allowed type is rejected with 415 and code attachment_content_mismatch.
  • External-URL attachments must use an http(s) scheme.

Signed URLs require an object-storage backend that actually signs its URLs (S3/MinIO, GCS, or Azure Blob via django-storages — see Configuration). On FileSystemStorage (the default) or an unrecognized backend, the signed-url action returns 501 rather than a link claiming an expires_at it can’t honor.

Logging time requires Team Member role or above on the task’s project (can_log_time); a Viewer who reads a task sees can_log_time: false. Every entry is owned by the logged-in user — the owner is server-set and cannot be supplied in the request body. Each contributor sees only their own hours; there is no cross-contributor rollup in the community edition.

MethodPathDescription
POST/api/v1/tasks/{task_pk}/time-entries/Log time against a task (minutes 1–1440, optional entry_date, note); Member+
GET/api/v1/tasks/{task_pk}/time-entries/The caller’s own entries on the task plus total_logged_minutes; Viewer+ (theirs may be empty)
PATCH/api/v1/me/time-entries/{id}/Edit minutes / entry_date / note — author only (others get 404)
DELETE/api/v1/me/time-entries/{id}/Soft-delete an entry — author only
GET/api/v1/me/time-entries/?from=&to=Weekly cross-project rollup (results + totals.by_day / by_cell / today_minutes / week_minutes); defaults to the current week
GET/api/v1/me/timer/The caller’s running timer with server-computed elapsed_seconds / stale, or {active: false}
POST/api/v1/me/timer/startStart a timer ({task, note?}); a second start atomically stops and logs the running timer first, returning it as finalized_entry; Member+
POST/api/v1/me/timer/stopStop the running timer and log it as a TimeEntry (source: "timer"); 409 if no timer is running

A manual entry_date cannot be in the future, nor older than the backdate window (TIMETRACKING_BACKDATE_DAYS, default 60 days). A timer left running past the stale ceiling (TIMETRACKING_TIMER_MAX_MINUTES, default 600) is flagged stale: true, and on stop its logged minutes are capped at the ceiling rather than the raw elapsed time.

MethodPathDescription
POST/api/v1/sprints/{id}/promote-to-milestone/Bind the sprint’s commitment to a schedule milestone so sprint velocity reforecasts the CPM finish
POST/api/v1/sprints/{id}/unbind-milestone/Remove the binding between the sprint and its milestone

See Sprint–milestone rollup for the UI workflow and error codes.

MethodPathDescription
GET/api/v1/dependencies/List (filter: ?project=, ?dep_type=FS, ?task=)
POST/api/v1/dependencies/Create
GET/api/v1/dependencies/{id}/Retrieve
PUT / PATCH/api/v1/dependencies/{id}/Update
DELETE/api/v1/dependencies/{id}/Soft-delete
POST/api/v1/dependencies/{id}/accept/Accept a pending cross-project edge (downstream Resource Manager+)
POST/api/v1/dependencies/{id}/reject/Reject (soft-delete) a pending cross-project edge

Predecessor and successor may belong to the same project or to two projects in the same program. Cross-program edges return HTTP 400 (the Enterprise boundary is unchanged). A cross-project edge whose successor sits in a project the creator cannot schedule is created pending: it is inert until the downstream project’s Resource Manager+ accepts it via accept/. Once accepted, the program’s schedule recomputes across the boundary so floats and criticality are program-true on every member project’s own schedule (not only the program schedule view).

When an accepted cross-project dependency pushes a committed task in an active sprint past its sprint boundary, the program recompute records a slip conflict for the downstream team. The dates stay honest — the firewall never moves a sprint, its membership, or its commitment math; it only surfaces the conflict for the team to acknowledge and resolve their own way.

MethodPathDescription
GET/api/v1/slip-conflicts/List (filter: ?program=, ?project=, ?sprint=, ?open=true) — scoped to your member projects
GET/api/v1/slip-conflicts/{id}/Retrieve
POST/api/v1/slip-conflicts/{id}/acknowledge/Acknowledge (downstream Scrum Master / Product Owner facet, or Admin+)

Acknowledgment is an audit act — “seen, handling it” — not a schedule change; only a member of the threatened project with the Scrum Master / Product Owner facet (or Admin+) may acknowledge. A conflict that stops slipping (the task moves out, the sprint is extended, the edge is rejected) auto-resolves on the next recompute.

MethodPathDescription
POST/api/v1/projects/{id}/monte-carlo/Run a probabilistic schedule simulation synchronously and return P50/P80/P95 (no state written)
GET/api/v1/projects/{id}/monte-carlo/latest/Retrieve the most recently recorded simulation run for the project
GET/api/v1/projects/{id}/monte-carlo/history/List recorded simulation runs for the project

The run endpoint accepts an optional n_simulations in the body; it must not exceed the OSS simulation cap or the request returns 402. See Monte Carlo and the MC_* caps in Configuration.

MethodPathDescription
GET/api/v1/resources/List (per-user throttle: 60 req/min)
POST/api/v1/resources/Create
GET/api/v1/resources/{id}/Retrieve
PUT / PATCH/api/v1/resources/{id}/Update
DELETE/api/v1/resources/{id}/Soft-delete

The resource catalog is readable by any authenticated user, so the email field is gated to prevent org-wide address harvesting: org admins (Admin or Owner on any project, or superusers) receive email on every row, and a caller always sees their own email (is_me: true). For all other callers the email field is omitted from the payload entirely. A per-user throttle of 60 req/min applies to the list endpoint to bound bulk scraping; exceeding it returns 429 Too Many Requests.

MethodPathDescription
GET/api/v1/task-resources/List
POST/api/v1/task-resources/Assign
GET/api/v1/task-resources/{id}/Retrieve
PUT / PATCH/api/v1/task-resources/{id}/Update
DELETE/api/v1/task-resources/{id}/Remove
MethodPathDescription
GET/api/v1/project-resources/List the roster (filter: ?project=)
POST/api/v1/project-resources/Add a resource to a project’s roster (Scheduler+)
GET/api/v1/project-resources/{id}/Retrieve
PUT / PATCH/api/v1/project-resources/{id}/Update (Scheduler+)
DELETE/api/v1/project-resources/{id}/Remove from roster (Scheduler+)
DELETE/api/v1/project-resources/{id}/?force=trueForce-remove and cascade-delete the resource’s task assignments

A plain DELETE returns 409 Conflict with code has_assignments if the resource has live task assignments on the project; the response body lists the affected_tasks, a sample of task_names, and the assignment_count. Passing ?force=true cascades the deletion to the resource’s TaskResource rows on the project and triggers a CPM recalculation for the affected tasks. All write and delete operations require the Scheduler role or higher on the project.

MethodPathAuthDescription
GET/api/v1/workspace/Any active memberRetrieve workspace config.
PATCH/api/v1/workspace/Workspace Admin+Update workspace config (partial).

The workspace config includes public_sharing and allow_guests (the inheritance defaults for all programs and projects) and public_sharing_override_policy (suggest/enforce). enforce is an Enterprise-only lock and degrades to suggest in the community edition. See Sharing & Access Inheritance.

The workspace config also carries the attachment policy root — the non-null top of the Workspace → Program → Project inheritance chain (lower scopes leave their override null to inherit these):

FieldTypeMeaning
attachments_enabledbooleanWhether task file uploads are permitted by default (external links are unaffected).
allowed_attachment_typesstring[]MIME allow-list (seeded from the system default). An empty list is a deliberate “no file types allowed” policy.
attachments_override_policystringinherit / suggest / enforce (default suggest). enforce is an Enterprise lock and is a no-op in the community edition.

These three fields are writable by a Workspace Admin+. Writing a permanently security-denied MIME type (text/html, image/svg+xml, application/xhtml+xml) into allowed_attachment_types returns 400 — these can never be allowed.

MethodPathAuthDescription
GET/api/v1/workspace/members/Admin+ (non-admin sees own row only)List workspace members.
PATCH/api/v1/workspace/members/{user_id}/Admin+Change a member’s role or status.
DELETE/api/v1/workspace/members/{user_id}/Admin+Deactivate a member.
MethodPathAuthDescription
GET/api/v1/workspace/invites/Admin+List pending invites.
POST/api/v1/workspace/invites/Admin+Create an invite (email queued asynchronously).
DELETE/api/v1/workspace/invites/{id}/Admin+Revoke a pending invite.
POST/api/v1/workspace/invites/accept/PublicAccept an invite with a one-time token. Rate-limited: 20 req/min.

See Workspace Settings for invite token security and the group-access cascade.

MethodPathAuthDescription
GET/api/v1/workspace/groups/Any memberList groups.
POST/api/v1/workspace/groups/Admin+Create a group.
GET/api/v1/workspace/groups/{id}/Any memberRetrieve a group.
PATCH/api/v1/workspace/groups/{id}/Admin+Update name, description, or lead.
DELETE/api/v1/workspace/groups/{id}/Admin+Delete group (removes group-conferred memberships).
POST/api/v1/workspace/groups/{id}/members/Admin+Add a member (triggers project-access cascade).
DELETE/api/v1/workspace/groups/{id}/members/{user_id}/Admin+Remove a member (triggers cascade).
POST/api/v1/workspace/groups/{id}/projects/Admin+Link group to a project with a conferred role (triggers cascade).
DELETE/api/v1/workspace/groups/{id}/projects/{project_id}/Admin+Unlink group from a project (removes group-conferred memberships).

Webhooks are scoped to a project or a program:

MethodPathDescription
GET/api/v1/projects/{id}/webhooks/List (also /programs/{id}/webhooks/)
POST/api/v1/projects/{id}/webhooks/Create
GET/api/v1/projects/{id}/webhooks/{wid}/Retrieve
PUT / PATCH/api/v1/projects/{id}/webhooks/{wid}/Update
DELETE/api/v1/projects/{id}/webhooks/{wid}/Delete

The signing secret (used to HMAC-sign delivered payloads) is write-only and follows a one-time-secret model:

  • It is never returned on GET, list, or update responses.
  • It is echoed back exactly once, in the 201 Created response body, so the caller can record it. Refetching the webhook afterward never exposes it again — if the secret is lost it must be rotated by supplying a new one.
  • If omitted or left blank on create, a cryptographically strong secret is auto-generated (token_urlsafe(32), ~43 URL-safe characters) and returned in that one-time create response.
  • A supplied secret must be at least 32 characters. A whitespace-only value is rejected; blank is treated as “auto-generate”. Validation failures return 400.
MethodPathDescription
GET/api/v1/projects/{id}/sync/Pull delta changes
POST/api/v1/projects/{id}/sync/Push a batch of offline changes

See Offline Sync.

The push endpoint accepts a batch of created / updated / deleted task rows with a client-generated client_batch_id for idempotent replay. Two boundary rules:

  • Idempotency replay is scoped per actor. A repeated batch with the same client_batch_id from the same authenticated user replays the original stored response. A different user who reuses the same client_batch_id gets a fresh batch — never the original actor’s response — because the stored response carries that actor’s task ids, server versions, and sync watermark. Replay never crosses the project or the user boundary.
  • Cross-project id collisions return 409. A created row whose client-generated id collides with a task that lives in another project returns 409 Conflict (code sync_id_collision). The client must regenerate the id and re-upload — the server will not silently mutate a task in a project the caller’s URL scope does not own.

Default page size: 50. Response envelope:

{"count": 123, "next": "...?page=3", "previous": "...?page=1", "results": [...]}
CodeMeaning
200OK
201Created
204No content (delete)
400Validation error
401Missing or invalid token
403Insufficient role
404Not found or soft-deleted
409Conflict (e.g. duplicate membership, sync id collision)
429Rate limit exceeded (e.g. resource catalog throttle)