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/Authentication
Section titled “Authentication”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).
Log in
Section titled “Log in”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>Refresh the access token
Section titled “Refresh the 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/TokenRefreshRequestbody schemas are intentionally gone from the OpenAPI document — the refresh endpoint takes no request body.
Log out
Section titled “Log out”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.
| Setting | Default | Purpose |
|---|---|---|
TRUEPPM_AUTH_REFRESH_COOKIE_NAME | trueppm_refresh | Cookie 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_SAMESITE | Strict | CSRF posture — the cookie is never sent cross-site. |
TRUEPPM_AUTH_REFRESH_COOKIE_SECURE | True | HTTPS-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.
Endpoints
Section titled “Endpoints”Calendars
Section titled “Calendars”| Method | Path | Description |
|---|---|---|
| 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 |
Projects
Section titled “Projects”| Method | Path | Description |
|---|---|---|
| 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:
| Field | Type | Meaning |
|---|---|---|
attachments_enabled | boolean | null | Whether file uploads are permitted. null = inherit from the parent scope. |
allowed_attachment_types | string[] | null | MIME 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.
Project members
Section titled “Project members”| Method | Path | Description |
|---|---|---|
| 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.
Programs
Section titled “Programs”A program is a container for related projects (see Programs).
| Method | Path | Description |
|---|---|---|
| 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.
| Method | Path | Description |
|---|---|---|
| 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.
Task attachments
Section titled “Task attachments”Each attachment is either an uploaded file or an external URL — never both.
| Method | Path | Description |
|---|---|---|
| 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_enabledisfalse, a file upload returns403. External-URL attachments are not affected byattachments_enabled. - The uploaded file’s MIME type must be in the project’s resolved allow-list
(not a fixed list). A disallowed type returns
415with codeattachment_unsupported_mime. The declared MIME is also content-sniffed against the real bytes, so a payload that masquerades as an allowed type is rejected with415and codeattachment_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.
Time tracking
Section titled “Time tracking”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.
| Method | Path | Description |
|---|---|---|
| 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/start | Start 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/stop | Stop 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.
Sprint–milestone binding
Section titled “Sprint–milestone binding”| Method | Path | Description |
|---|---|---|
| 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.
Dependencies
Section titled “Dependencies”| Method | Path | Description |
|---|---|---|
| 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).
Cross-project slip conflicts
Section titled “Cross-project slip conflicts”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.
| Method | Path | Description |
|---|---|---|
| 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.
Monte Carlo
Section titled “Monte Carlo”| Method | Path | Description |
|---|---|---|
| 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.
Resources
Section titled “Resources”| Method | Path | Description |
|---|---|---|
| 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.
Task-resource assignments
Section titled “Task-resource assignments”| Method | Path | Description |
|---|---|---|
| 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 |
Project resource roster
Section titled “Project resource roster”| Method | Path | Description |
|---|---|---|
| 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=true | Force-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.
Workspace
Section titled “Workspace”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/workspace/ | Any active member | Retrieve 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):
| Field | Type | Meaning |
|---|---|---|
attachments_enabled | boolean | Whether task file uploads are permitted by default (external links are unaffected). |
allowed_attachment_types | string[] | MIME allow-list (seeded from the system default). An empty list is a deliberate “no file types allowed” policy. |
attachments_override_policy | string | inherit / 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.
Workspace members
Section titled “Workspace members”| Method | Path | Auth | Description |
|---|---|---|---|
| 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. |
Workspace invites
Section titled “Workspace invites”| Method | Path | Auth | Description |
|---|---|---|---|
| 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/ | Public | Accept an invite with a one-time token. Rate-limited: 20 req/min. |
See Workspace Settings for invite token security and the group-access cascade.
Workspace groups
Section titled “Workspace groups”| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/v1/workspace/groups/ | Any member | List groups. |
| POST | /api/v1/workspace/groups/ | Admin+ | Create a group. |
| GET | /api/v1/workspace/groups/{id}/ | Any member | Retrieve 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
Section titled “Webhooks”Webhooks are scoped to a project or a program:
| Method | Path | Description |
|---|---|---|
| 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 Createdresponse 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.
| Method | Path | Description |
|---|---|---|
| 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_idfrom the same authenticated user replays the original stored response. A different user who reuses the sameclient_batch_idgets 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. Acreatedrow whose client-generatedidcollides with a task that lives in another project returns409 Conflict(codesync_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.
Pagination
Section titled “Pagination”Default page size: 50. Response envelope:
{"count": 123, "next": "...?page=3", "previous": "...?page=1", "results": [...]}Status codes
Section titled “Status codes”| Code | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 204 | No content (delete) |
| 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) |
| 429 | Rate limit exceeded (e.g. resource catalog throttle) |