Projects API
Projects are keyed by slug, not by UUID, in every project-scoped path. A slug
is 3..50 lowercase letters, digits, or hyphens and must start and end with a
letter or digit. The UUID still exists as project_id in payloads, but the path
parameter and the Z4J agent config both use the slug.
List my projects
Section titled “List my projects”GET /api/v1/projectsReturns projects the authenticated user has a role in. Global brain admins are
the exception: they see every active project regardless of membership, up to
Z4J_ADMIN_PROJECT_LIST_CAP. When the caller uses a project-scoped bearer key,
the response is further filtered to that bound project.
[ { "id": "...", "slug": "billing-prod", "name": "Billing (production)", "description": null, "environment": "production", "timezone": "UTC", "is_active": true, "default_scheduler_owner": "z4j-scheduler", "allowed_schedulers": null, "created_at": "...", "updated_at": "..." }]Get project
Section titled “Get project”GET /api/v1/projects/{slug}Role: viewer. Returns the same ProjectPublic shape.
Create project
Section titled “Create project”POST /api/v1/projectsRole: global brain admin (is_admin=true), not merely project admin.
CSRF-protected. The first admin is created through first-boot setup, not this
endpoint.
{ "slug": "billing-prod", "name": "Billing (production)", "description": null, "environment": "production", "timezone": "UTC", "default_scheduler_owner": "z4j-scheduler", "allowed_schedulers": null}Field rules:
slug: 3..50 lowercase letters / digits / hyphens, starting and ending with a letter or digit. The request model checks only length 2..63 (422outside it); a slug that fails the format is refused with409 conflict, the same status as a slug already in use.environment: 1..40 chars matching^[a-z][a-z0-9_-]{0,39}$; canonical values areproduction,staging,development,test, but free-form is accepted so values such asstaging-eucontinue to work.default_scheduler_owner: the scheduler that owns new schedules when the caller did not pick one. Free-form (40 char cap, lowercase pattern). Defaultz4j-scheduler.allowed_schedulers: optional allow-list of scheduler names.nullmeans unrestricted; an empty list is rejected. Capped at 32 entries. When set, it must containdefault_scheduler_owner.
Update project
Section titled “Update project”PATCH /api/v1/projects/{slug}Role: global brain admin. CSRF-protected. Any subset of the create fields,
including the slug itself. Only keys sent with a non-null value are applied,
so description cannot be cleared back to null through this route;
allowed_schedulers is the exception, where an explicit null removes the
restriction. A slug rename takes effect immediately: saved REST
URLs, bookmarks, CLI --slug values, and schedule-import manifests using the
old slug start returning 404. Running agents and project-scoped API keys remain
bound by project UUID and continue to work.
Delete project
Section titled “Delete project”DELETE /api/v1/projects/{slug}Role: global brain admin. CSRF-protected and requires fresh MFA, so this route
requires a browser session rather than an API key. Delete is a soft archive: it
sets is_active=false and leaves agents, tasks, events, schedules, memberships,
and audit rows intact. The slug remains taken. Archiving the final active
project is refused.
Archiving also disconnects the project's agents. Live WebSocket sessions close
with code 4003 on every replica, the same close a revoked agent gets, so the
shipped agent parks on its auth backoff. A handshake from an agent of an
archived project is refused with 4401, and the long-poll routes answer 403
with error: project_inactive. The agent rows and their tokens are untouched:
once is_active is true again the agents reconnect on their next retry with
their existing tokens, without re-minting. Each refused handshake is audited as
agent.auth.project_inactive against the agent.