# impartire API — Reference: Projects

Every path is under `https://impartire.com/api/v1`. The machine-readable contract is https://impartire.com/api/v1/openapi.json (OpenAPI 3.1).

#### GET /api/v1/projects

List the org's projects

Ordered by name unless `sort` says otherwise. To poll for changes, pass `updated_after` with the newest `updated_at` you have seen and `sort=-updated_at` (or `updated_at`). A deleted project leaves the list.

Scopes: `projects:read`

Parameters:

- `limit` (query, integer) — Page size, default 50. Values above 100 are treated as 100.
- `cursor` (query, string) — next_cursor from the previous page, with the other parameters unchanged.
- `parent_id` (query, string (uuid)) — Only the direct children of this project.
- `updated_after` (query, string (date-time)) — Only projects changed strictly after this instant (ISO 8601 with offset), compared to the millisecond — poll with the newest `updated_at` you have seen (that project does not come back), sorted `-updated_at` or `updated_at`.
- `sort` (query, "name" | "-name" | "updated_at" | "-updated_at") — Sort key; a leading `-` is descending. Default `name`. Ties break on id.

Response 200 — `ProjectList`

- `data` (array of Project, required)
  - `id` (string (uuid), required)
  - `name` (string, required)
  - `description` (string or null, required)
  - `parent_id` (string (uuid) or null, required)
  - `visibility` (string, required) — open | closed | private
  - `project_type` (object or null, required) — The project's type (the org's project types: production, season, campaign…), or null if untyped.
    - `id` (string (uuid), required)
    - `name` (string, required)
  - `created_at` (string (date-time), required)
  - `updated_at` (string (date-time), required) — When its name, description or type last changed.
- `next_cursor` (string or null, required) — Pass as `cursor` to get the next page; null means this is the last page.

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope — the key lacks a required scope.
- 404 (Problem) — not-found.
- 422 (Problem) — validation — a bad `limit`, `parent_id`, `updated_after`, `sort` or `cursor`.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.

#### POST /api/v1/projects

Create a project, optionally typed and under a parent

A root project (no `parent_id`) is born Closed — org members see that it exists, admitted teams open it; a sub-project inherits its parent's access. The credential is the project's owner: for an API key, the key's service principal. Typed projects follow the org's hierarchy rules.

Scopes: `projects:write`

Parameters:

- `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters.

Request body — `CreateProjectRequest`

- `name` (string, required)
- `description` (string or null)
- `parent_id` (string (uuid)) — Create it as a sub-project of this project (GET /api/v1/projects). Omit for a root project (a production), which is born Closed: org members see it exists, admitted teams open it.
- `project_type_id` (string (uuid)) — One of the org's project types. When the parent is typed too, the org's hierarchy rules must allow this type under the parent's (a root needs a rule allowing the type at the root); 422 invalid-hierarchy otherwise.

Response 201 — `Project`

- `id` (string (uuid), required)
- `name` (string, required)
- `description` (string or null, required)
- `parent_id` (string (uuid) or null, required)
- `visibility` (string, required) — open | closed | private
- `project_type` (object or null, required) — The project's type (the org's project types: production, season, campaign…), or null if untyped.
  - `id` (string (uuid), required)
  - `name` (string, required)
- `created_at` (string (date-time), required)
- `updated_at` (string (date-time), required) — When its name, description or type last changed.

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope — the key lacks a required scope.
- 404 (Problem) — not-found.
- 409 (Problem) — idempotency-in-flight.
- 422 (Problem) — validation (name, description, parent_id, project_type_id), invalid-hierarchy, or idempotency-mismatch.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — authorization-write-failed — the project's permissions could not be written, so it was rolled back; or internal.

#### GET /api/v1/projects/{id}

Get one project

Scopes: `projects:read`

Parameters:

- `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects.
- `If-None-Match` (header, string) — An ETag from a previous response; 304 if unchanged.

Response 200 — `Project`

- `id` (string (uuid), required)
- `name` (string, required)
- `description` (string or null, required)
- `parent_id` (string (uuid) or null, required)
- `visibility` (string, required) — open | closed | private
- `project_type` (object or null, required) — The project's type (the org's project types: production, season, campaign…), or null if untyped.
  - `id` (string (uuid), required)
  - `name` (string, required)
- `created_at` (string (date-time), required)
- `updated_at` (string (date-time), required) — When its name, description or type last changed.

Other responses:

- 304 — Not modified — the ETag in If-None-Match is current.
- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope — the key lacks a required scope.
- 404 (Problem) — not-found.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.

#### PATCH /api/v1/projects/{id}

Rename, describe or retype a project

If-Match is required: send the ETag from the last GET (or from the create). A retype must fit the org's hierarchy rules under the parent's type and above each sub-project's type. Moving a project is POST /projects/{id}/move (guarded); changing its visibility is done in the app.

Scopes: `projects:write`

Parameters:

- `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects.
- `If-Match` (header, string, required) — The project's ETag.

Request body — `UpdateProjectRequest`: At least one of name, description, project_type_id. Moving a project and changing its visibility are not in v1.

- `name` (string)
- `description` (string or null) — null clears it.
- `project_type_id` (string (uuid) or null) — Retype the project; null makes it untyped. The org's hierarchy rules must allow the type under its parent's type (or at the root) and above each sub-project's type; 422 invalid-hierarchy otherwise.

Response 200 — `Project`

- `id` (string (uuid), required)
- `name` (string, required)
- `description` (string or null, required)
- `parent_id` (string (uuid) or null, required)
- `visibility` (string, required) — open | closed | private
- `project_type` (object or null, required) — The project's type (the org's project types: production, season, campaign…), or null if untyped.
  - `id` (string (uuid), required)
  - `name` (string, required)
- `created_at` (string (date-time), required)
- `updated_at` (string (date-time), required) — When its name, description or type last changed.

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope — the key lacks a required scope.
- 404 (Problem) — not-found.
- 412 (Problem) — precondition-failed — If-Match is stale; `etag` in the body is the current one.
- 422 (Problem) — validation (empty body, name, description, project_type_id), or invalid-hierarchy.
- 428 (Problem) — precondition-required — no If-Match.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.

#### POST /api/v1/projects/{id}/access

Give a member, team or role access to a project (guarded)

A guarded action (ADR 0081): it changes who can see the project. The app's own write runs, with its checks and words. **An API key's write happens at once.** **An OAuth token's does not**: 202 with a Confirmation naming the project and who, by name; once its person confirms it in impartire it runs as them, every check again. Its person must manage the project (project manage; no platform-admin bypass). The subject must be a member, team or role of the org (422), a team not archived and a subject not denied on the project (409: lift the deny first). A team's or role's grant follows its membership. Audited: a `project.access_granted` event.

Scopes: `access:write`

Parameters:

- `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects.
- `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters.

Request body — `GrantProjectAccessRequest`

- `subject_type` ("member" | "team" | "role", required) — member: one person (GET /api/v1/members). team: everyone in the team. role: everyone holding the role. A team's or role's grant follows its membership.
- `subject_id` (string (uuid), required) — The member's principal id (GET /api/v1/members), or the team's or role's id — of this org.
- `level` ("viewer" | "asset_contributor" | "asset_purger" | "contributor" | "manager", required) — viewer: read the project and everything filed under it. contributor: edit the project too. manager: administer it, including who has access. asset_contributor: edit the assets filed under it WITHOUT reading the project. asset_purger: delete them, likewise.

Response 201 — `ProjectGrant`: A project access grant: who, at which level.

- `project` (object, required) — The project.
  - `id` (string (uuid), required)
  - `name` (string, required)
- `subject` (object, required)
  - `type` ("member" | "team" | "role", required) — member: one person (GET /api/v1/members). team: everyone in the team. role: everyone holding the role. A team's or role's grant follows its membership.
  - `id` (string (uuid), required) — The member's principal id, or the team's or role's id.
  - `name` (string or null, required) — The member's name, or the team's or role's. Null when it is no longer in the org.
- `level` ("viewer" | "asset_contributor" | "asset_purger" | "contributor" | "manager", required) — viewer: read the project and everything filed under it. contributor: edit the project too. manager: administer it, including who has access. asset_contributor: edit the assets filed under it WITHOUT reading the project. asset_purger: delete them, likewise.

Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired.

- `id` (string (uuid), required)
- `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire).
- `action` (string, required) — The guarded action, e.g. `run.signal`.
- `resource` (object, required)
  - `type` (string, required) — The kind of resource the action is on, e.g. `run`.
  - `id` (string (uuid) or null, required)
  - `name` (string, required) — The resource as the person was shown it when the action was asked for.
- `summary` (string, required) — The action in words, as the person reads it on the confirmation page.
- `client_id` (string, required) — The OAuth client that asked.
- `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm.
- `created_at` (string (date-time), required)
- `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired.
- `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending.
- `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise.
- `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise.
  - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/<code>. Branch on this.
  - `title` (string, required)
  - `status` (integer, required)
  - `detail` (string, required) — What went wrong and what to do about it, written for the caller.
  - `instance` (string, required) — The request path.
  - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem.
  - `errors` (array of object) — On 422 validation: one entry per invalid field.
    - `path` (string, required) — The field or parameter, as a dotted path.
    - `message` (string, required)
  - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq.
  - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag.
  - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist).
  - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version.
  - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}).
  - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire.

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person can open the project but not manage it.
- 404 (Problem) — not-found.
- 409 (Problem) — conflict — an archived team, or a subject denied on the project; confirmation-pending; or idempotency-in-flight.
- 422 (Problem) — validation (a bad field; `subject_id` not of the org), or idempotency-mismatch (an OAuth token may not send Idempotency-Key).
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.
- 503 (Problem) — authorization-unavailable — the permission service could not be reached; nothing changed.

#### DELETE /api/v1/projects/{id}/access

Take one access grant on a project away (guarded)

A guarded action (ADR 0081): it changes who can see the project. The app's own write runs, with its checks and words. **An API key's write happens at once.** **An OAuth token's does not**: 202 with a Confirmation naming the project and who, by name; once its person confirms it in impartire it runs as them, every check again. Its person must manage the project. Names the grant by `subject_type`, `subject_id` and `level`; a subject that has since left the org can still be revoked. 404 when there is no such grant (nothing changed). Audited: a `project.access_revoked` event.

Scopes: `access:write`

Parameters:

- `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects.
- `subject_type` (query, "member" | "team" | "role", required) — member: one person (GET /api/v1/members). team: everyone in the team. role: everyone holding the role. A team's or role's grant follows its membership.
- `subject_id` (query, string (uuid), required) — The subject the grant names.
- `level` (query, "viewer" | "asset_contributor" | "asset_purger" | "contributor" | "manager", required) — viewer: read the project and everything filed under it. contributor: edit the project too. manager: administer it, including who has access. asset_contributor: edit the assets filed under it WITHOUT reading the project. asset_purger: delete them, likewise.

Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired.

- `id` (string (uuid), required)
- `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire).
- `action` (string, required) — The guarded action, e.g. `run.signal`.
- `resource` (object, required)
  - `type` (string, required) — The kind of resource the action is on, e.g. `run`.
  - `id` (string (uuid) or null, required)
  - `name` (string, required) — The resource as the person was shown it when the action was asked for.
- `summary` (string, required) — The action in words, as the person reads it on the confirmation page.
- `client_id` (string, required) — The OAuth client that asked.
- `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm.
- `created_at` (string (date-time), required)
- `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired.
- `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending.
- `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise.
- `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise.
  - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/<code>. Branch on this.
  - `title` (string, required)
  - `status` (integer, required)
  - `detail` (string, required) — What went wrong and what to do about it, written for the caller.
  - `instance` (string, required) — The request path.
  - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem.
  - `errors` (array of object) — On 422 validation: one entry per invalid field.
    - `path` (string, required) — The field or parameter, as a dotted path.
    - `message` (string, required)
  - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq.
  - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag.
  - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist).
  - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version.
  - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}).
  - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire.

Other responses:

- 204 — An API key's revoke: the grant is gone.
- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person cannot manage the project.
- 404 (Problem) — not-found.
- 409 (Problem) — confirmation-pending.
- 422 (Problem) — validation — a missing or bad `subject_type`, `subject_id` or `level`.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.
- 503 (Problem) — authorization-unavailable — the permission service could not be reached; nothing changed.

#### POST /api/v1/projects/{id}/deny

Deny a member, team or role on a project (guarded)

A guarded action (ADR 0081): it changes who can see the project. The app's own write runs, with its checks and words. **An API key's write happens at once.** **An OAuth token's does not**: 202 with a Confirmation naming the project and who, by name; once its person confirms it in impartire it runs as them, every check again. Its person must manage the project. A deny removes read — and everything above it — on the project and everything under it, whatever else grants it, admins included (ADR 0047). A `reason` is required. Not yourself (422: you could not lift it); the subject must be the org's (422); already denied is 409. Audited: a `project.access_denied` event (its reason is not served by the audit log).

Scopes: `access:write`

Parameters:

- `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects.
- `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters.

Request body — `DenyProjectAccessRequest`

- `subject_type` ("member" | "team" | "role", required) — member: one person (GET /api/v1/members). team: everyone in the team. role: everyone holding the role. A team's or role's grant follows its membership.
- `subject_id` (string (uuid), required) — The member's principal id, or the team's or role's id — of this org. Not yourself.
- `reason` (string, required) — Why. Required: a deny is a reasoned, recorded act.

Response 201 — `ProjectDeny`: A deny: the subject loses read (and everything above it) on the project and everything under it, whatever else grants it.

- `project` (object, required) — The project.
  - `id` (string (uuid), required)
  - `name` (string, required)
- `subject` (object, required)
  - `type` ("member" | "team" | "role", required) — member: one person (GET /api/v1/members). team: everyone in the team. role: everyone holding the role. A team's or role's grant follows its membership.
  - `id` (string (uuid), required) — The member's principal id, or the team's or role's id.
  - `name` (string or null, required) — The member's name, or the team's or role's. Null when it is no longer in the org.
- `reason` (string, required)

Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired.

- `id` (string (uuid), required)
- `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire).
- `action` (string, required) — The guarded action, e.g. `run.signal`.
- `resource` (object, required)
  - `type` (string, required) — The kind of resource the action is on, e.g. `run`.
  - `id` (string (uuid) or null, required)
  - `name` (string, required) — The resource as the person was shown it when the action was asked for.
- `summary` (string, required) — The action in words, as the person reads it on the confirmation page.
- `client_id` (string, required) — The OAuth client that asked.
- `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm.
- `created_at` (string (date-time), required)
- `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired.
- `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending.
- `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise.
- `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise.
  - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/<code>. Branch on this.
  - `title` (string, required)
  - `status` (integer, required)
  - `detail` (string, required) — What went wrong and what to do about it, written for the caller.
  - `instance` (string, required) — The request path.
  - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem.
  - `errors` (array of object) — On 422 validation: one entry per invalid field.
    - `path` (string, required) — The field or parameter, as a dotted path.
    - `message` (string, required)
  - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq.
  - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag.
  - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist).
  - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version.
  - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}).
  - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire.

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person cannot manage the project.
- 404 (Problem) — not-found.
- 409 (Problem) — conflict — already denied; confirmation-pending; or idempotency-in-flight.
- 422 (Problem) — validation (a bad field, yourself, a subject not of the org), or idempotency-mismatch.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.
- 503 (Problem) — authorization-unavailable — the permission service could not be reached; nothing changed.

#### DELETE /api/v1/projects/{id}/deny

Lift a deny on a project (guarded)

A guarded action (ADR 0081): it changes who can see the project. The app's own write runs, with its checks and words. **An API key's write happens at once.** **An OAuth token's does not**: 202 with a Confirmation naming the project and who, by name; once its person confirms it in impartire it runs as them, every check again. Its person must manage the project. The deny's record is kept, marked lifted. 404 when the subject has no active deny. Audited: a `project.deny_lifted` event.

Scopes: `access:write`

Parameters:

- `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects.
- `subject_type` (query, "member" | "team" | "role", required) — member: one person (GET /api/v1/members). team: everyone in the team. role: everyone holding the role. A team's or role's grant follows its membership.
- `subject_id` (query, string (uuid), required) — The denied subject.

Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired.

- `id` (string (uuid), required)
- `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire).
- `action` (string, required) — The guarded action, e.g. `run.signal`.
- `resource` (object, required)
  - `type` (string, required) — The kind of resource the action is on, e.g. `run`.
  - `id` (string (uuid) or null, required)
  - `name` (string, required) — The resource as the person was shown it when the action was asked for.
- `summary` (string, required) — The action in words, as the person reads it on the confirmation page.
- `client_id` (string, required) — The OAuth client that asked.
- `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm.
- `created_at` (string (date-time), required)
- `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired.
- `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending.
- `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise.
- `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise.
  - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/<code>. Branch on this.
  - `title` (string, required)
  - `status` (integer, required)
  - `detail` (string, required) — What went wrong and what to do about it, written for the caller.
  - `instance` (string, required) — The request path.
  - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem.
  - `errors` (array of object) — On 422 validation: one entry per invalid field.
    - `path` (string, required) — The field or parameter, as a dotted path.
    - `message` (string, required)
  - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq.
  - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag.
  - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist).
  - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version.
  - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}).
  - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire.

Other responses:

- 204 — An API key's lift: the deny no longer applies.
- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person cannot manage the project.
- 404 (Problem) — not-found.
- 409 (Problem) — confirmation-pending.
- 422 (Problem) — validation — a missing or bad `subject_type` or `subject_id`.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.
- 503 (Problem) — authorization-unavailable — the deny is recorded as lifted but may still be enforced; deny again, then lift once the service is back.

#### POST /api/v1/projects/{id}/teams

Assign a team to a production (guarded)

A guarded action (ADR 0081): it changes who can see the project. The app's own write runs, with its checks and words. **An API key's write happens at once.** **An OAuth token's does not**: 202 with a Confirmation naming the project and who, by name; once its person confirms it in impartire it runs as them, every check again. Its person must administer the production (project administrate). The project must be a top-level project nobody owns and not itself a lane; the team the org's and not archived (422). The team's members read the production and get their own lane on it — a project the team owns, private to it. Idempotent: 201 when the lane was made, 200 when the team was already assigned. There is no unassign in the API (nor in the app). Audited: a `project.team_bound` event and the lane's capture.

Scopes: `access:write`

Parameters:

- `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects.
- `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters.

Request body — `BindProjectTeamRequest`

- `team_id` (string (uuid), required) — A team of this org that is not archived.

Response 200 — `ProductionLane`: A team assigned to a production, and its lane.

- `production` (object, required) — The production (the project).
  - `id` (string (uuid), required)
  - `name` (string, required)
- `team` (object, required)
  - `id` (string (uuid), required)
  - `name` (string, required)
- `lane` (object, required) — The team's lane on the production: a project the team owns, private to it, attached to the production without inheriting its audience.
  - `id` (string (uuid), required)
  - `name` (string, required)
- `created` (boolean, required) — False when the team was already assigned (the lane is the existing one).

Response 201 — `ProductionLane`: A team assigned to a production, and its lane.

- `production` (object, required) — The production (the project).
  - `id` (string (uuid), required)
  - `name` (string, required)
- `team` (object, required)
  - `id` (string (uuid), required)
  - `name` (string, required)
- `lane` (object, required) — The team's lane on the production: a project the team owns, private to it, attached to the production without inheriting its audience.
  - `id` (string (uuid), required)
  - `name` (string, required)
- `created` (boolean, required) — False when the team was already assigned (the lane is the existing one).

Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired.

- `id` (string (uuid), required)
- `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire).
- `action` (string, required) — The guarded action, e.g. `run.signal`.
- `resource` (object, required)
  - `type` (string, required) — The kind of resource the action is on, e.g. `run`.
  - `id` (string (uuid) or null, required)
  - `name` (string, required) — The resource as the person was shown it when the action was asked for.
- `summary` (string, required) — The action in words, as the person reads it on the confirmation page.
- `client_id` (string, required) — The OAuth client that asked.
- `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm.
- `created_at` (string (date-time), required)
- `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired.
- `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending.
- `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise.
- `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise.
  - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/<code>. Branch on this.
  - `title` (string, required)
  - `status` (integer, required)
  - `detail` (string, required) — What went wrong and what to do about it, written for the caller.
  - `instance` (string, required) — The request path.
  - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem.
  - `errors` (array of object) — On 422 validation: one entry per invalid field.
    - `path` (string, required) — The field or parameter, as a dotted path.
    - `message` (string, required)
  - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq.
  - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag.
  - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist).
  - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version.
  - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}).
  - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire.

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person does not administer the production.
- 404 (Problem) — not-found.
- 409 (Problem) — confirmation-pending, or idempotency-in-flight.
- 422 (Problem) — validation — `team_id` not a team of the org, an archived team, or a project that cannot be a production; idempotency-mismatch.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.
- 503 (Problem) — authorization-unavailable — the permission service could not be reached; retry (assigning again is safe).

#### POST /api/v1/projects/{id}/move

Move a project under another, or to the top level (guarded)

A guarded action (ADR 0081): a project reads through its permission root, so moving it changes who can see it. The app's own move runs: Unfiled, the Library, a lane (nesting) and a team space (nesting) stay put (409); the parent must be a project of the org (422), not inside the moved subtree, allowed by the org's hierarchy rules and no more than five levels deep (422 invalid-hierarchy); and a move may narrow or keep the project's audience, never widen it (422 — widen a root from its settings page in the app). A move that narrows it revokes the public share links to the assets under it. Its person must be able to update the project and the new parent (no platform-admin bypass). **An API key's move happens at once**: 200 with the Project. **An OAuth token's does not**: 202 with a Confirmation saying where it goes and what that does; once its person confirms it in impartire it runs as them, every check again. Moving a project to where it already is answers 200 at once, for either credential. Audited: a `project.moved` event and the row's capture.

Scopes: `access:write`

Parameters:

- `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects.
- `Idempotency-Key` (header, string) — Makes a retry safe: the same key and request within 24 hours replays the first response. 1–255 characters.

Request body — `MoveProjectRequest`

- `parent_id` (string (uuid) or null, required) — The new parent project (GET /api/v1/projects), or null to make it a top-level project.

Response 200 — `Project`

- `id` (string (uuid), required)
- `name` (string, required)
- `description` (string or null, required)
- `parent_id` (string (uuid) or null, required)
- `visibility` (string, required) — open | closed | private
- `project_type` (object or null, required) — The project's type (the org's project types: production, season, campaign…), or null if untyped.
  - `id` (string (uuid), required)
  - `name` (string, required)
- `created_at` (string (date-time), required)
- `updated_at` (string (date-time), required) — When its name, description or type last changed.

Response 202 — `Confirmation`: A guarded action an OAuth token asked for, waiting for its person to confirm it in impartire (ADR 0081). A guarded endpoint answers 202 with this and a Location; poll it until `status` is executed, declined or expired.

- `id` (string (uuid), required)
- `status` ("pending" | "confirmed" | "declined" | "expired" | "executed", required) — pending: waiting for the person to confirm or decline it in impartire (at `confirm_url`). confirmed: they confirmed and the action is running — poll again. executed: it ran; `result` holds its answer, or `problem` why it failed. declined: they said no; it will never run. expired: nobody decided within 15 minutes; it will never run (GET /api/v1/confirmations/{id} answers 410 confirmation-expired instead of this body; the person still sees it in impartire).
- `action` (string, required) — The guarded action, e.g. `run.signal`.
- `resource` (object, required)
  - `type` (string, required) — The kind of resource the action is on, e.g. `run`.
  - `id` (string (uuid) or null, required)
  - `name` (string, required) — The resource as the person was shown it when the action was asked for.
- `summary` (string, required) — The action in words, as the person reads it on the confirmation page.
- `client_id` (string, required) — The OAuth client that asked.
- `confirm_url` (string, required) — Where the person confirms or declines it. Show it to them; the API cannot confirm.
- `created_at` (string (date-time), required)
- `expires_at` (string (date-time), required) — 15 minutes after it was asked for. A pending confirmation past it is expired.
- `decided_at` (string (date-time) or null, required) — When the person confirmed or declined it, or when it expired; null while pending.
- `result` (any, required) — When executed and the action succeeded: the action's answer (what the endpoint would have returned with a key). Null otherwise.
- `problem` (Problem or null, required) — When executed and the action failed: the Problem it failed with. Null otherwise.
  - `type` (string, required) — A stable URL naming the problem: https://impartire.com/problems/<code>. Branch on this.
  - `title` (string, required)
  - `status` (integer, required)
  - `detail` (string, required) — What went wrong and what to do about it, written for the caller.
  - `instance` (string, required) — The request path.
  - `request_id` (string, required) — Also sent as the Request-Id header. Quote it when reporting a problem.
  - `errors` (array of object) — On 422 validation: one entry per invalid field.
    - `path` (string, required) — The field or parameter, as a dotted path.
    - `message` (string, required)
  - `edit_seq` (integer) — On 412 precondition-failed: the resource's current edit_seq.
  - `etag` (string) — On 412 precondition-failed for a project, a collection, a playlist or a schedule slot: its current ETag.
  - `item_id` (string) — On 409 already-member: the existing item's id (in the collection or playlist).
  - `schema_version` (integer) — On 409 type-changed: the asset type's current schema_version.
  - `confirmation_id` (string) — On 409 confirmation-pending: the confirmation already waiting for its person (GET /api/v1/confirmations/{id}).
  - `confirm_url` (string) — On 409 confirmation-pending: where the person confirms or declines it, in impartire.

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope, or insufficient-permission — the token's person cannot update the project or the new parent.
- 404 (Problem) — not-found.
- 409 (Problem) — conflict — an infrastructure project; confirmation-pending; or idempotency-in-flight.
- 422 (Problem) — validation (`parent_id` not a project of the org, its own subtree, or a move that would widen who can see it), invalid-hierarchy, or idempotency-mismatch.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.
- 503 (Problem) — authorization-unavailable — the permission service could not be reached; nothing changed.

#### GET /api/v1/projects/{id}/tags

List the tags on a project

A project's own tags: its sub-projects and assets do not inherit them. At most 500, ordered by the tag's normalized name; no cursor.

Scopes: `projects:read`

Parameters:

- `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects.

Response 200 — `TagLinkList`

- `data` (array of TagLink, required) — Ordered by the tag's normalized name. At most 500.
  - `id` (string (uuid), required) — The tag's id — what DELETE …/tags/{tag_id} and `{ id }` on POST …/tags take.
  - `name` (string, required) — The tag as first typed. Tags are matched by a normalized form: case, extra spaces and invisible characters are ignored.
  - `scope` ("org" | "team", required) — `org`: the whole org's. `team`: private to one team (an OAuth token sees it only when its person is in the team or an org admin).
  - `team` (object or null, required) — The owning team, for a team tag; null for an org tag.
    - `id` (string (uuid), required)
    - `name` (string, required)
  - `usage_count` (integer, required) — How many assets and projects carry the tag, across the org.
  - `source` ("user" | "generated", required) — `user`: a person or an API credential added it. `generated`: applied from a generated summary.
  - `added_at` (string (date-time), required)
- `next_cursor` (string or null, required) — Always null: the list is returned whole (at most 500).

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope — the key lacks a required scope.
- 404 (Problem) — not-found.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.

#### POST /api/v1/projects/{id}/tags

Add tags to a project

Each item is `{ name }` — the org's tag of that name, created as an org tag when there is none — or `{ id }` of an existing tag (the only way to add a team tag). A tag already on the project stays as it was, so the call is safe to repeat: no Idempotency-Key, no If-Match. Answers the project's whole tag list after. An OAuth token's person needs update on the project.

Scopes: `projects:write`

Parameters:

- `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects.

Request body — `AddTagsRequest`

- `tags` (array of object or object, required) — 1–50 tags, each `{ name }` or `{ id }`.
  - `name` (string, required) — A tag name. The org's tag of that name is used, or created as an org tag if there is none.

Response 200 — `TagLinkList`

- `data` (array of TagLink, required) — Ordered by the tag's normalized name. At most 500.
  - `id` (string (uuid), required) — The tag's id — what DELETE …/tags/{tag_id} and `{ id }` on POST …/tags take.
  - `name` (string, required) — The tag as first typed. Tags are matched by a normalized form: case, extra spaces and invisible characters are ignored.
  - `scope` ("org" | "team", required) — `org`: the whole org's. `team`: private to one team (an OAuth token sees it only when its person is in the team or an org admin).
  - `team` (object or null, required) — The owning team, for a team tag; null for an org tag.
    - `id` (string (uuid), required)
    - `name` (string, required)
  - `usage_count` (integer, required) — How many assets and projects carry the tag, across the org.
  - `source` ("user" | "generated", required) — `user`: a person or an API credential added it. `generated`: applied from a generated summary.
  - `added_at` (string (date-time), required)
- `next_cursor` (string or null, required) — Always null: the list is returned whole (at most 500).

Other responses:

- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person may see the project but lacks update on the project.
- 404 (Problem) — not-found.
- 422 (Problem) — validation (`tags`: 1–50 items, each `{ name }` or `{ id }`; a name that is empty or over 64 characters), or unknown-tag.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.

#### DELETE /api/v1/projects/{id}/tags/{tag_id}

Remove a tag from a project

Takes the tag off the project; the tag stays in the org's list. 404 when the project does not carry it, so a second DELETE is a 404.

Scopes: `projects:write`

Parameters:

- `id` (path, string (uuid), required) — The project's id, from GET /api/v1/projects.
- `tag_id` (path, string (uuid), required) — The tag's id, from GET …/tags.

Other responses:

- 204 — Removed.
- 401 (Problem) — unauthenticated — no key, or an unknown, revoked or expired one.
- 403 (Problem) — insufficient-scope, or insufficient-permission — an OAuth token's person lacks update on the project.
- 404 (Problem) — not-found.
- 429 (Problem) — rate-limited — the key's request budget is spent; wait Retry-After seconds.
- 500 (Problem) — internal.
