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/
  • API authentication — logging in, refreshing and revoking access tokens, project-scoped API tokens, personal access tokens, and single sign-on.
  • Projects, members and workspace API — calendars, projects, project members, and workspace members, invites and groups.
  • Programs API — programs, program membership sync, and the program backlog.
  • Tasks and dependencies API — tasks, attachments, dependencies, cross-project slip conflicts, Monte Carlo, task relations, acceptance criteria and recurrence rules.
  • Project templates and import API — project templates, import templates, and import provenance.
  • Sprints and estimation API — sprint cadence generation, sprint–milestone binding, scope and duration changes, estimation poker, retro items and velocity suggestions.
  • Resources and time API — resources, task-resource assignments, the project roster, teams, skills, and time tracking.
  • Webhooks, sync and sharing API — webhooks, offline sync, integrations, notification preferences, mention groups, assets, public share links, agent actions and user search.

These pages are a curated tour, not an exhaustive endpoint dump — the interactive schema is complete, and several groups get only a pointer to the feature page that covers them.

/api/v1/admin/failed-tasks/ (list, retrieve, requeue, drop, requeue-all, drop-all) is the Celery dead-letter queue surface, fully documented in System health.

MethodPathDescription
GET/api/v1/readyzDependency-aware readiness for Kubernetes readiness/startup probes. Unauthenticated by design (kubelet sends no credentials). Coarse {"status": "ok"|"fail", "checks": {...}, "migration_state": "in_sync"} body with no infrastructure detail; 200 when every checked dependency is healthy, 503 otherwise

migration_state reports which way the pod’s migrations differ from the recorded schema: behind (this image ships migrations the database has not applied — the rolling-forward window), ahead (the database records migrations this image does not ship), unknown (the check itself failed), or in_sync.

behind and unknown always make the pod not-ready. ahead gates the pod only when it booted into that state — the rollback case. A pod that booted in sync and drifted to ahead while serving is the ordinary rolling upgrade, and stays ready so the Service is not emptied while the new pods come up; the state is still reported either way. An operator can also re-open the gated case with TRUEPPM_READYZ_ALLOW_DB_AHEAD for a deliberate additive-only rollback, which changes readiness without hiding the state. checks keeps its existing keys and its ok/fail values, so a scraper reading only status/checks is unaffected. ahead is a schema-presence signal, not a data-compatibility verdict — see rollback.

Distinct from the plain unauthenticated liveness check at /api/v1/health/ and the admin-only, deeper /health/system/ used by System health — readyz is the one Kubernetes should point a readiness probe at.

Default page size: 50. Response envelope:

{"count": 123, "next": "...?page=3", "previous": "...?page=1", "results": [...]}

Endpoints over unbounded, append-only logs are cursor-paginated instead — the audit log, the workspace member list, and a webhook’s delivery history. A cursor envelope has no count (computing one would defeat the point of a cursor), so it is {next, previous, results} only. Follow next until it is null; do not compute page counts from these endpoints.

Not every list endpoint is paginated. Some return a bare array where the result set is inherently small and bounded — a task’s relations, for instance. The generated OpenAPI schema is authoritative per endpoint: check the declared 200 response shape rather than assuming an envelope.

Every endpoint is rate limited. A general default applies to any endpoint that does not declare a stricter, endpoint-specific limit:

CallerDefault limitBucketed by
Unauthenticated60 requests / minuteClient IP
Authenticated1000 requests / minuteAccount

Both defaults are operator-configurable (TRUEPPM_THROTTLE_ANON_RATE and TRUEPPM_THROTTLE_USER_RATE; see Configuration).

  • /api/v1/health/ and /api/v1/edition/ are exempt. They are never rate limited, so a Kubernetes liveness loop is not throttled. /api/v1/readyz is not exempt — unlike those two it does a real database and cache round-trip per call, so it gets its own dedicated, generous scope (readyz in the table below) instead of a full exemption, to bound an unauthenticated caller who reaches the pod IP directly (the probe path bypasses the Ingress).
  • Scoped endpoints replace the default. An endpoint with its own limit carries only that specific limit — scoped limits do not stack on top of the general default (two different scoped throttles on the same endpoint, e.g. task-sync’s per-project limit, do stack with each other).

Two mechanisms implement a scoped limit. Most are a named entry in DEFAULT_THROTTLE_RATES (env-tunable via the TRUEPPM_THROTTLE_* variable named alongside each one below, where one exists); a handful of endpoints that need bespoke logic (a sliding ramp-up window, two stacked buckets, a Redis-atomic counter) are hand-rolled throttle classes instead. Both kinds return 429 with the same Retry-After envelope shown above.

Scoped rates (DEFAULT_THROTTLE_RATES):

ScopeRateApplies to
anon60/min (TRUEPPM_THROTTLE_ANON_RATE)General default, unauthenticated
user1000/min (TRUEPPM_THROTTLE_USER_RATE)General default, authenticated
readyz2000/min (TRUEPPM_THROTTLE_READYZ_RATE)/api/v1/readyz — its own scope, not the shared anon bucket, since its traffic comes from kubelet hitting the pod IP directly rather than through the Ingress
login10/minLogin, per client IP
login_account5/min (TRUEPPM_THROTTLE_LOGIN_ACCOUNT_RATE)Login, per submitted username — stacks with login
password_reset5/minPassword-reset request + confirm
refresh60/minJWT refresh
user_search60/minMember-invite user typeahead
omni_search60/min (TRUEPPM_THROTTLE_OMNI_SEARCH_RATE)⌘K Epic/Story omni-search
resolve120/min (TRUEPPM_THROTTLE_RESOLVE_RATE)Key resolver and key suggestion (/resolve/, /keys/), per account
ws_ticket120/minWebSocket connection-ticket minting
invite_resend5/minWorkspace invite resend
email_settings12/minWorkspace SMTP config writes
email_settings_probe6/minSMTP send-test + deliverability probe
oidc_discover30/minSSO domain discovery
oidc_login20/minSSO login start
oidc_callback30/minSSO callback
sso_test_connection20/minSSO admin “Test connection”
sso_provider_write20/minSSO provider create/update/delete (reads exempt)
credential_rotate10/minPersonal integration credentials + Git webhook secret rotation
external_sync20/minManual external-connection pull trigger
monte_carlo10/minSynchronous Monte Carlo run
monte_carlo_whatif6/minMonte Carlo what-if (two CPM + two MC passes per call)
burn60/min (TRUEPPM_THROTTLE_BURN_RATE)Burn chart + flow metrics reads (shared bucket — both replay HistoricalTask)
sample_load6/min (TRUEPPM_THROTTLE_SAMPLE_LOAD_RATE)Bundled-sample demo loader
seed_import6/min (TRUEPPM_THROTTLE_SEED_IMPORT_RATE)Caller-supplied program seed import
seed_validate20/min (TRUEPPM_THROTTLE_SEED_VALIDATE_RATE)Seed import dry run
sample_download60/min (TRUEPPM_THROTTLE_SAMPLE_DOWNLOAD_RATE)Bundled-fixture file download
mcp_read120/min (TRUEPPM_THROTTLE_MCP_READ_RATE)Per-token baseline on any MCP-readable view
mcp_read_compute12/min (TRUEPPM_THROTTLE_MCP_READ_COMPUTE_RATE)Stacks on mcp_read for the four compute-heavy MCP tools
share_mint20/min (TRUEPPM_THROTTLE_SHARE_MINT_RATE)Minting a public board/schedule share link
share_access60/min (TRUEPPM_THROTTLE_SHARE_ACCESS_RATE)Resolving a public share link
telemetry_test6/minTelemetry test-export probe
git_webhook_ip600/min (TRUEPPM_THROTTLE_GIT_WEBHOOK_IP_RATE)Inbound Git webhook receiver, per client IP — stacks with GitWebhookThrottle below

Hand-rolled throttle classes (custom windows/keys DRF’s scope rates can’t express):

ClassRateApplies to
TaskSyncThrottle100/min steady, 1000/min in the first 60 min after token mintInbound task-sync, per project
AcceptanceResultThrottleSame ramp as TaskSyncThrottleCI acceptance-result ingest, per token
TokenIssuanceThrottle5/min (TRUEPPM_TOKEN_ISSUANCE_PER_MINUTE)Minting any API token, per user
TaskAttachmentUploadThrottle60/minTask-attachment upload, per user
SyncUploadThrottle60/min per (project, user) and 120/min per userOffline sync push — fails closed (429) on a Redis outage, the one throttle in this table that does
GitWebhookThrottle120/minInbound Git webhook receiver, per project — the caller picks the project ID out of the URL, so this is stacked with the per-IP git_webhook_ip scope above and neither bounds the endpoint alone
TaskLinkRefreshThrottle30/minManual task-link refresh, per user
MentionRateThrottle100/hour and 1000/dayComment @mention fan-out, per user (both windows apply)
MembershipGrantThrottle60/minGranting project or program membership, per user — shared budget across both surfaces (rate set inline, not DEFAULT_THROTTLE_RATES); does not bound role changes or removal

Every hand-rolled class fails open on a Redis error (never blocks legitimate traffic during a cache outage) except SyncUploadThrottle, which fails closed — a denied offline sync retries with backoff, while an unbounded write-path throttle bypass during an outage was judged the worse failure mode.

When a caller exceeds a limit the API responds with 429 Too Many Requests and a Retry-After header giving the number of seconds to wait before retrying:

HTTP/1.1 429 Too Many Requests
Retry-After: 42
Content-Type: application/json
{"detail": "Request was throttled. Expected available in 42 seconds."}

Clients should honor Retry-After and back off; retrying before it elapses consumes no additional quota but continues to return 429.

CodeMeaning
200OK
201Created
202Accepted — the work was queued and runs asynchronously (e.g. MS Project / Jira / CSV import, workspace/program/project export, invite-email (re)queueing, a task-run cancellation request). The response carries a job/status resource to poll, or a bare {"queued": true}, not the final result
204No content (delete)
207Multi-status — the rows of a batch were applied independently, so the body reports applied, rejected, and skipped together rather than one verdict for the whole request (batch task writes). A 207 does not mean every row succeeded: always read rejected
304Not modified — the caller’s If-None-Match matched the current ETag (public share-link resolution; bundled-sample file download); the body is empty, refetch is unnecessary
400Validation error
401Missing or invalid token
403Insufficient role
404Not found or soft-deleted
409Conflict (e.g. duplicate membership, sync id collision)
410Gone — the resource existed but is deliberately no longer reachable and never will be again: a revoked public share link, or a completed workspace/program/project export download link past its expires_at. Distinct from 404 (never existed, or the caller cannot see it)
413Payload too large — the workspace branding-logo upload exceeds its 2 MB ceiling, or an inbound Git webhook body exceeds its 1 MB cap
415Unsupported media type (attachment upload outside the MIME allow-list)
422Well-formed but unprocessable (idempotency-key reuse, program-schedule limits)
429Rate limit exceeded — general default or a scoped throttle; includes a Retry-After header
501Not implemented by this deployment’s configured backend
502An upstream identity provider could not be reached
503Service unavailable — a dependency-aware readiness/health check reports a failing dependency (/api/v1/readyz, /health/system/, /health/beat/), or an aggregation endpoint’s per-section subservice failed and degraded gracefully (e.g. the integrations-summary view, whose body then carries a failed key naming the section so the client falls back to that section’s own endpoint)

Errors come in two shapes: field-keyed validation messages with no machine code, and structured bodies carrying a stable code you can branch on. See Errors and status codes for the full code table, the extra keys each carries, and which of them the stability contract covers.