API Reference
The TruePPM REST API is documented via OpenAPI 3.0.3, auto-generated by drf-spectacular.
Interactive schema
Section titled “Interactive schema”| Format | URL |
|---|---|
| Swagger UI | http://localhost:8000/api/schema/swagger-ui/ |
| Raw YAML | http://localhost:8000/api/schema/ |
Base URL
Section titled “Base URL”http://localhost:8000/api/v1/In this section
Section titled “In this section”- 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.
Admin: failed tasks
Section titled “Admin: failed tasks”/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.
Readiness probe
Section titled “Readiness probe”| Method | Path | Description |
|---|---|---|
| GET | /api/v1/readyz | Dependency-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.
Pagination
Section titled “Pagination”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.
Rate limiting
Section titled “Rate limiting”Every endpoint is rate limited. A general default applies to any endpoint that does not declare a stricter, endpoint-specific limit:
| Caller | Default limit | Bucketed by |
|---|---|---|
| Unauthenticated | 60 requests / minute | Client IP |
| Authenticated | 1000 requests / minute | Account |
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/readyzis not exempt — unlike those two it does a real database and cache round-trip per call, so it gets its own dedicated, generous scope (readyzin 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).
Complete scoped-throttle list
Section titled “Complete scoped-throttle list”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):
| Scope | Rate | Applies to |
|---|---|---|
anon | 60/min (TRUEPPM_THROTTLE_ANON_RATE) | General default, unauthenticated |
user | 1000/min (TRUEPPM_THROTTLE_USER_RATE) | General default, authenticated |
readyz | 2000/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 |
login | 10/min | Login, per client IP |
login_account | 5/min (TRUEPPM_THROTTLE_LOGIN_ACCOUNT_RATE) | Login, per submitted username — stacks with login |
password_reset | 5/min | Password-reset request + confirm |
refresh | 60/min | JWT refresh |
user_search | 60/min | Member-invite user typeahead |
omni_search | 60/min (TRUEPPM_THROTTLE_OMNI_SEARCH_RATE) | ⌘K Epic/Story omni-search |
resolve | 120/min (TRUEPPM_THROTTLE_RESOLVE_RATE) | Key resolver and key suggestion (/resolve/, /keys/), per account |
ws_ticket | 120/min | WebSocket connection-ticket minting |
invite_resend | 5/min | Workspace invite resend |
email_settings | 12/min | Workspace SMTP config writes |
email_settings_probe | 6/min | SMTP send-test + deliverability probe |
oidc_discover | 30/min | SSO domain discovery |
oidc_login | 20/min | SSO login start |
oidc_callback | 30/min | SSO callback |
sso_test_connection | 20/min | SSO admin “Test connection” |
sso_provider_write | 20/min | SSO provider create/update/delete (reads exempt) |
credential_rotate | 10/min | Personal integration credentials + Git webhook secret rotation |
external_sync | 20/min | Manual external-connection pull trigger |
monte_carlo | 10/min | Synchronous Monte Carlo run |
monte_carlo_whatif | 6/min | Monte Carlo what-if (two CPM + two MC passes per call) |
burn | 60/min (TRUEPPM_THROTTLE_BURN_RATE) | Burn chart + flow metrics reads (shared bucket — both replay HistoricalTask) |
sample_load | 6/min (TRUEPPM_THROTTLE_SAMPLE_LOAD_RATE) | Bundled-sample demo loader |
seed_import | 6/min (TRUEPPM_THROTTLE_SEED_IMPORT_RATE) | Caller-supplied program seed import |
seed_validate | 20/min (TRUEPPM_THROTTLE_SEED_VALIDATE_RATE) | Seed import dry run |
sample_download | 60/min (TRUEPPM_THROTTLE_SAMPLE_DOWNLOAD_RATE) | Bundled-fixture file download |
mcp_read | 120/min (TRUEPPM_THROTTLE_MCP_READ_RATE) | Per-token baseline on any MCP-readable view |
mcp_read_compute | 12/min (TRUEPPM_THROTTLE_MCP_READ_COMPUTE_RATE) | Stacks on mcp_read for the four compute-heavy MCP tools |
share_mint | 20/min (TRUEPPM_THROTTLE_SHARE_MINT_RATE) | Minting a public board/schedule share link |
share_access | 60/min (TRUEPPM_THROTTLE_SHARE_ACCESS_RATE) | Resolving a public share link |
telemetry_test | 6/min | Telemetry test-export probe |
git_webhook_ip | 600/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):
| Class | Rate | Applies to |
|---|---|---|
TaskSyncThrottle | 100/min steady, 1000/min in the first 60 min after token mint | Inbound task-sync, per project |
AcceptanceResultThrottle | Same ramp as TaskSyncThrottle | CI acceptance-result ingest, per token |
TokenIssuanceThrottle | 5/min (TRUEPPM_TOKEN_ISSUANCE_PER_MINUTE) | Minting any API token, per user |
TaskAttachmentUploadThrottle | 60/min | Task-attachment upload, per user |
SyncUploadThrottle | 60/min per (project, user) and 120/min per user | Offline sync push — fails closed (429) on a Redis outage, the one throttle in this table that does |
GitWebhookThrottle | 120/min | Inbound 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 |
TaskLinkRefreshThrottle | 30/min | Manual task-link refresh, per user |
MentionRateThrottle | 100/hour and 1000/day | Comment @mention fan-out, per user (both windows apply) |
MembershipGrantThrottle | 60/min | Granting 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 RequestsRetry-After: 42Content-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.
Status codes
Section titled “Status codes”| Code | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 202 | Accepted — 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 |
| 204 | No content (delete) |
| 207 | Multi-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 |
| 304 | Not 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 |
| 400 | Validation error |
| 401 | Missing or invalid token |
| 403 | Insufficient role |
| 404 | Not found or soft-deleted |
| 409 | Conflict (e.g. duplicate membership, sync id collision) |
| 410 | Gone — 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) |
| 413 | Payload too large — the workspace branding-logo upload exceeds its 2 MB ceiling, or an inbound Git webhook body exceeds its 1 MB cap |
| 415 | Unsupported media type (attachment upload outside the MIME allow-list) |
| 422 | Well-formed but unprocessable (idempotency-key reuse, program-schedule limits) |
| 429 | Rate limit exceeded — general default or a scoped throttle; includes a Retry-After header |
| 501 | Not implemented by this deployment’s configured backend |
| 502 | An upstream identity provider could not be reached |
| 503 | Service 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.