Zaroz
Zaroz Cloud Kernel Open API

List My Organizations

GET
/api/v2/organizations

Lists the organizations the caller is a member of.

List My Organizations › Responses

One organization as seen by its own member, on the `/v2` console surface. `/v2` DTOs are always named `*Request` / `*Response` (design doc §43.4) -- there is no `OrganizationView` here on purpose.
created_at
​string · date-time · required
id
​string · uuid · required
name
​string · required
slug
​string · required

URL-safe identifier, unique platform-wide. Console routes are /{slug}/...

billing_country_code
​string | null

Two-letter country the organization is taxed under. Null until set; a purchase in a shared organization needs it (PUT /organizations/{id}/billing-country).

icon_asset_id
​string | null · uuid

The organization's uploaded icon (ZC-1305), addressed through /api/v2/assets/{id}. None when no icon has been uploaded. An asset id, not a URL -- the /v2 convention every other asset-backed field on this surface already follows (see OrganizationMemberResponse::avatar_asset_id's own doc comment): the id itself changes on every replace, so a consumer building the URL from it never has to append a cache-busting query parameter to avoid serving a stale image.

​

The caller's role in this organization, when resolved.


Create Organization

POST
/api/v2/organizations

Creates a new organization owned by the caller. The caller is enrolled as its Owner.

Create Organization › Request Body

Body of `POST /organizations`.
name
​string · required
slug
​string · required

URL-safe identifier, unique platform-wide (lowercase ascii letters, digits, and single internal hyphens).

Create Organization › Responses

One organization as seen by its own member, on the /v2 console surface. /v2 DTOs are always named *Request / *Response (design doc §43.4) -- there is no OrganizationView here on purpose.

One organization as seen by its own member, on the `/v2` console surface. `/v2` DTOs are always named `*Request` / `*Response` (design doc §43.4) -- there is no `OrganizationView` here on purpose.
created_at
​string · date-time · required
id
​string · uuid · required
name
​string · required
slug
​string · required

URL-safe identifier, unique platform-wide. Console routes are /{slug}/...

billing_country_code
​string | null

Two-letter country the organization is taxed under. Null until set; a purchase in a shared organization needs it (PUT /organizations/{id}/billing-country).

icon_asset_id
​string | null · uuid

The organization's uploaded icon (ZC-1305), addressed through /api/v2/assets/{id}. None when no icon has been uploaded. An asset id, not a URL -- the /v2 convention every other asset-backed field on this surface already follows (see OrganizationMemberResponse::avatar_asset_id's own doc comment): the id itself changes on every replace, so a consumer building the URL from it never has to append a cache-busting query parameter to avoid serving a stale image.

​

The caller's role in this organization, when resolved.


Get Organization

GET
/api/v2/organizations/{id}

Gets one organization the caller belongs to.

Get Organization › path Parameters

id
​string · uuid · required · style: simple

Get Organization › Responses

One organization as seen by its own member, on the /v2 console surface. /v2 DTOs are always named *Request / *Response (design doc §43.4) -- there is no OrganizationView here on purpose.

One organization as seen by its own member, on the `/v2` console surface. `/v2` DTOs are always named `*Request` / `*Response` (design doc §43.4) -- there is no `OrganizationView` here on purpose.
created_at
​string · date-time · required
id
​string · uuid · required
name
​string · required
slug
​string · required

URL-safe identifier, unique platform-wide. Console routes are /{slug}/...

billing_country_code
​string | null

Two-letter country the organization is taxed under. Null until set; a purchase in a shared organization needs it (PUT /organizations/{id}/billing-country).

icon_asset_id
​string | null · uuid

The organization's uploaded icon (ZC-1305), addressed through /api/v2/assets/{id}. None when no icon has been uploaded. An asset id, not a URL -- the /v2 convention every other asset-backed field on this surface already follows (see OrganizationMemberResponse::avatar_asset_id's own doc comment): the id itself changes on every replace, so a consumer building the URL from it never has to append a cache-busting query parameter to avoid serving a stale image.

​

The caller's role in this organization, when resolved.


Delete Organization

DELETE
/api/v2/organizations/{id}

Deletes an organization the caller owns. Only the organization's Owner may delete it. Refuses to delete a personal organization, refuses if the organization still holds any project -- delete every project first -- and refuses while its wallet still holds a balance.

Delete Organization › path Parameters

id
​string · uuid · required · style: simple

Delete Organization › Responses

Deleted

No data returned

Update Organization

PATCH
/api/v2/organizations/{id}

Renames and/or re-slugs an organization the caller owns or administers. Omitting a field leaves it unchanged. A personal organization's name and slug cannot be changed.

Update Organization › path Parameters

id
​string · uuid · required · style: simple

Update Organization › Request Body

Body of `PATCH /organizations/{id}`. `name` and `slug` are each independently optional -- `None` means "leave unchanged" for both, matching PATCH semantics. A personal organization refuses to change either.
name
​string | null
Default: null
slug
​string | null
Default: null

Update Organization › Responses

One organization as seen by its own member, on the /v2 console surface. /v2 DTOs are always named *Request / *Response (design doc §43.4) -- there is no OrganizationView here on purpose.

One organization as seen by its own member, on the `/v2` console surface. `/v2` DTOs are always named `*Request` / `*Response` (design doc §43.4) -- there is no `OrganizationView` here on purpose.
created_at
​string · date-time · required
id
​string · uuid · required
name
​string · required
slug
​string · required

URL-safe identifier, unique platform-wide. Console routes are /{slug}/...

billing_country_code
​string | null

Two-letter country the organization is taxed under. Null until set; a purchase in a shared organization needs it (PUT /organizations/{id}/billing-country).

icon_asset_id
​string | null · uuid

The organization's uploaded icon (ZC-1305), addressed through /api/v2/assets/{id}. None when no icon has been uploaded. An asset id, not a URL -- the /v2 convention every other asset-backed field on this surface already follows (see OrganizationMemberResponse::avatar_asset_id's own doc comment): the id itself changes on every replace, so a consumer building the URL from it never has to append a cache-busting query parameter to avoid serving a stale image.

​

The caller's role in this organization, when resolved.


Set Organization Billing Country

PUT
/api/v2/organizations/{id}/billing-country

Records the country this organization is taxed under, which a purchase inside a shared organization needs (a quote answers billing-country-required until it is set). Owners and admins only. Set once: repeating the same country succeeds, a different one is refused with billing-country-already-set.

Set Organization Billing Country › path Parameters

id
​string · uuid · required · style: simple

Set Organization Billing Country › Request Body

Body of `PUT /organizations/{id}/billing-country` (V-0864).
country_code
​string · required

Two-letter country code, e.g. ES. Case-insensitive.

Set Organization Billing Country › Responses

One organization as seen by its own member, on the /v2 console surface. /v2 DTOs are always named *Request / *Response (design doc §43.4) -- there is no OrganizationView here on purpose.

One organization as seen by its own member, on the `/v2` console surface. `/v2` DTOs are always named `*Request` / `*Response` (design doc §43.4) -- there is no `OrganizationView` here on purpose.
created_at
​string · date-time · required
id
​string · uuid · required
name
​string · required
slug
​string · required

URL-safe identifier, unique platform-wide. Console routes are /{slug}/...

billing_country_code
​string | null

Two-letter country the organization is taxed under. Null until set; a purchase in a shared organization needs it (PUT /organizations/{id}/billing-country).

icon_asset_id
​string | null · uuid

The organization's uploaded icon (ZC-1305), addressed through /api/v2/assets/{id}. None when no icon has been uploaded. An asset id, not a URL -- the /v2 convention every other asset-backed field on this surface already follows (see OrganizationMemberResponse::avatar_asset_id's own doc comment): the id itself changes on every replace, so a consumer building the URL from it never has to append a cache-busting query parameter to avoid serving a stale image.

​

The caller's role in this organization, when resolved.


Get Organization By Slug

GET
/api/v2/organizations/by-slug/{slug}

Gets one organization the caller belongs to, by its slug.

Get Organization By Slug › path Parameters

slug
​string · required · style: simple

Get Organization By Slug › Responses

One organization as seen by its own member, on the /v2 console surface. /v2 DTOs are always named *Request / *Response (design doc §43.4) -- there is no OrganizationView here on purpose.

One organization as seen by its own member, on the `/v2` console surface. `/v2` DTOs are always named `*Request` / `*Response` (design doc §43.4) -- there is no `OrganizationView` here on purpose.
created_at
​string · date-time · required
id
​string · uuid · required
name
​string · required
slug
​string · required

URL-safe identifier, unique platform-wide. Console routes are /{slug}/...

billing_country_code
​string | null

Two-letter country the organization is taxed under. Null until set; a purchase in a shared organization needs it (PUT /organizations/{id}/billing-country).

icon_asset_id
​string | null · uuid

The organization's uploaded icon (ZC-1305), addressed through /api/v2/assets/{id}. None when no icon has been uploaded. An asset id, not a URL -- the /v2 convention every other asset-backed field on this surface already follows (see OrganizationMemberResponse::avatar_asset_id's own doc comment): the id itself changes on every replace, so a consumer building the URL from it never has to append a cache-busting query parameter to avoid serving a stale image.

​

The caller's role in this organization, when resolved.


Upload Organization Icon

PUT
/api/v2/organizations/{id}/icon

Uploads an organization's icon. Send as multipart/form-data with the image in a part named file: JPEG, PNG, WebP or static GIF (first frame), identified by magic bytes; anything else, including SVG, is refused. Re-encoded server-side to a 512x512 lossy WebP with EXIF orientation applied and all other metadata stripped; the original bytes are never stored. Every upload -- including a replace -- gets a FRESH asset id, so the returned icon_asset_id always changes and a client caching by that id never serves a stale image. The previous icon is released asynchronously. Requires ManageOrganizationPolicy (the same bar PATCH uses). Rate-limited per caller.

Upload Organization Icon › path Parameters

id
​string · uuid · required · style: simple

Upload Organization Icon › Request Body

file
​string · binary · required

Upload Organization Icon › Responses

The organization, with its new icon_asset_id

One organization as seen by its own member, on the `/v2` console surface. `/v2` DTOs are always named `*Request` / `*Response` (design doc §43.4) -- there is no `OrganizationView` here on purpose.
created_at
​string · date-time · required
id
​string · uuid · required
name
​string · required
slug
​string · required

URL-safe identifier, unique platform-wide. Console routes are /{slug}/...

billing_country_code
​string | null

Two-letter country the organization is taxed under. Null until set; a purchase in a shared organization needs it (PUT /organizations/{id}/billing-country).

icon_asset_id
​string | null · uuid

The organization's uploaded icon (ZC-1305), addressed through /api/v2/assets/{id}. None when no icon has been uploaded. An asset id, not a URL -- the /v2 convention every other asset-backed field on this surface already follows (see OrganizationMemberResponse::avatar_asset_id's own doc comment): the id itself changes on every replace, so a consumer building the URL from it never has to append a cache-busting query parameter to avoid serving a stale image.

​

The caller's role in this organization, when resolved.


Remove Organization Icon

DELETE
/api/v2/organizations/{id}/icon

Removes an organization's uploaded icon. A no-op success (204) when the organization had no icon. Requires ManageOrganizationPolicy.

Remove Organization Icon › path Parameters

id
​string · uuid · required · style: simple

Remove Organization Icon › Responses

Removed

No data returned

List Organization Members

GET
/api/v2/organizations/{id}/members

Lists the members of an organization the caller belongs to, each with their name, email address and profile picture. Returns not found rather than forbidden for an organization the caller may not read, so an organization that exists and one that never did are indistinguishable to a non-member.

List Organization Members › path Parameters

id
​string · uuid · required · style: simple

List Organization Members › Responses

Members

One membership row, carrying enough of the member's identity that the person reading it can tell **who** it is. This used to be `organization_id`, `user_id`, `role` and `created_at` alone, which meant console's members table could only render a raw uuid -- an organization owner literally could not tell who their own collaborators were (`console/components/members/member-cell.tsx`, and `console/openapi/PENDING.md`, both said so). Owner ruling 2026-09-09 item 8: name, email **and** profile picture, all three, visible to fellow members. Three shape decisions, each with a precedent in this workspace: - **Denormalized onto the membership row, not a separate identity lookup.** A lookup route would have to answer for a `user_id` in isolation, which is a user-enumeration oracle by construction: it cannot know the caller shares an organization with the subject without being handed the organization, at which point it is this route with extra steps. Resolving at response time is what `kernel_http_v2_admin::contracts::organizations` already does. - **`email` is `Option`**, following `UserSummaryResponse`: an account with no reachable address is a real state (see [`UserEmailService`](kernel_core::application::services::users::user_email_service::UserEmailService)), not a failed lookup, and a member list must not 500 because one member signed up through a provider that returned no address. - **`avatar_asset_id`, not an `avatar_url`.** The `/v2` rule is asset ids on the wire, addressed through `/api/v2/assets/{id}` by the consumer (`kernel_http_v2_reseller::contracts::reseller`, `kernel_http_v2_catalog::contracts::products`'s `icon_asset_id`). `/v1` baked `/api/v1/assets/{id}?v={updated_at}` into responses and pinned the wire to a path `/v2` deletes. It also matters here for a second reason: an id is an opaque kernel uuid, so no part of this field is derived from the member's email address the way a Gravatar-style hash or an OAuth provider's CDN URL would be. The provider's avatar is downloaded into our own object storage at login (`AvatarImportService`), so a member's picture never sends a viewer to a third party.
created_at
​string · date-time · required
name
​string · required

The member's display name.

organization_id
​string · uuid · required
role
​OrganizationRoleResponse · enum · required
Enum values:
owner
admin
billing
member
user_id
​string · uuid · required
avatar_asset_id
​string | null · uuid

The member's profile picture, addressed through /api/v2/assets/{id}. None when they never set one.

email
​string | null

None when the account has no reachable address.


Add Organization Member

POST
/api/v2/organizations/{id}/members

Adds a user to an organization the caller owns or administers. Re-adding an existing member returns that membership unchanged. A personal organization takes no new members (422 member-personal-organization).

Add Organization Member › path Parameters

id
​string · uuid · required · style: simple

Add Organization Member › Request Body

Body of `POST /organizations/{id}/members`.
user_id
​string · uuid · required

The user to add to this organization.

​

Defaults to member, the least-privileged role: an omitted role must never widen access further than the caller asked for.

Add Organization Member › Responses

One membership row, carrying enough of the member's identity that the person reading it can tell who it is.

This used to be organization_id, user_id, role and created_at alone, which meant console's members table could only render a raw uuid -- an organization owner literally could not tell who their own collaborators were (console/components/members/member-cell.tsx, and console/openapi/PENDING.md, both said so). Owner ruling 2026-09-09 item 8: name, email and profile picture, all three, visible to fellow members.

Three shape decisions, each with a precedent in this workspace:

  • Denormalized onto the membership row, not a separate identity lookup. A lookup route would have to answer for a user_id in isolation, which is a user-enumeration oracle by construction: it cannot know the caller shares an organization with the subject without being handed the organization, at which point it is this route with extra steps. Resolving at response time is what kernel_http_v2_admin::contracts::organizations already does.
  • email is Option, following UserSummaryResponse: an account with no reachable address is a real state (see UserEmailService), not a failed lookup, and a member list must not 500 because one member signed up through a provider that returned no address.
  • avatar_asset_id, not an avatar_url. The /v2 rule is asset ids on the wire, addressed through /api/v2/assets/{id} by the consumer (kernel_http_v2_reseller::contracts::reseller, kernel_http_v2_catalog::contracts::products's icon_asset_id). /v1 baked /api/v1/assets/{id}?v={updated_at} into responses and pinned the wire to a path /v2 deletes. It also matters here for a second reason: an id is an opaque kernel uuid, so no part of this field is derived from the member's email address the way a Gravatar-style hash or an OAuth provider's CDN URL would be. The provider's avatar is downloaded into our own object storage at login (AvatarImportService), so a member's picture never sends a viewer to a third party.
One membership row, carrying enough of the member's identity that the person reading it can tell **who** it is. This used to be `organization_id`, `user_id`, `role` and `created_at` alone, which meant console's members table could only render a raw uuid -- an organization owner literally could not tell who their own collaborators were (`console/components/members/member-cell.tsx`, and `console/openapi/PENDING.md`, both said so). Owner ruling 2026-09-09 item 8: name, email **and** profile picture, all three, visible to fellow members. Three shape decisions, each with a precedent in this workspace: - **Denormalized onto the membership row, not a separate identity lookup.** A lookup route would have to answer for a `user_id` in isolation, which is a user-enumeration oracle by construction: it cannot know the caller shares an organization with the subject without being handed the organization, at which point it is this route with extra steps. Resolving at response time is what `kernel_http_v2_admin::contracts::organizations` already does. - **`email` is `Option`**, following `UserSummaryResponse`: an account with no reachable address is a real state (see [`UserEmailService`](kernel_core::application::services::users::user_email_service::UserEmailService)), not a failed lookup, and a member list must not 500 because one member signed up through a provider that returned no address. - **`avatar_asset_id`, not an `avatar_url`.** The `/v2` rule is asset ids on the wire, addressed through `/api/v2/assets/{id}` by the consumer (`kernel_http_v2_reseller::contracts::reseller`, `kernel_http_v2_catalog::contracts::products`'s `icon_asset_id`). `/v1` baked `/api/v1/assets/{id}?v={updated_at}` into responses and pinned the wire to a path `/v2` deletes. It also matters here for a second reason: an id is an opaque kernel uuid, so no part of this field is derived from the member's email address the way a Gravatar-style hash or an OAuth provider's CDN URL would be. The provider's avatar is downloaded into our own object storage at login (`AvatarImportService`), so a member's picture never sends a viewer to a third party.
created_at
​string · date-time · required
name
​string · required

The member's display name.

organization_id
​string · uuid · required
role
​OrganizationRoleResponse · enum · required
Enum values:
owner
admin
billing
member
user_id
​string · uuid · required
avatar_asset_id
​string | null · uuid

The member's profile picture, addressed through /api/v2/assets/{id}. None when they never set one.

email
​string | null

None when the account has no reachable address.


Remove Organization Member

DELETE
/api/v2/organizations/{id}/members/{user_id}

Removes a member from an organization. A caller may always remove themselves. Removing anyone else needs Owner or Admin. Refuses to remove the organization's personal owner or its last remaining Owner, and refuses to remove a member whose role is above the caller's own (403 member-outranks-caller).

Remove Organization Member › path Parameters

id
​string · uuid · required · style: simple
user_id
​string · uuid · required · style: simple

Remove Organization Member › Responses

Removed

No data returned

Update Organization Member

PATCH
/api/v2/organizations/{id}/members/{user_id}

Changes the role of a member of an organization the caller owns or administers. Refuses to change the organization's personal owner's role, refuses to demote the organization's last remaining Owner, and refuses to change a member whose role is above the caller's own (403 member-outranks-caller).

Update Organization Member › path Parameters

id
​string · uuid · required · style: simple
user_id
​string · uuid · required · style: simple

Update Organization Member › Request Body

Body of `PATCH /organizations/{id}/members/{user_id}`. Unlike adding a member, `role` has no default here: silently defaulting a role-change request would either be a no-op or an unrequested demotion, neither of which the caller asked for.
role
​OrganizationRoleRequest · enum · required

Distinct from OrganizationRoleResponse because request and response enums are separate types on /v2 (design doc §43.4), even where they currently carry the same variants.

Enum values:
owner
admin
billing
member

Update Organization Member › Responses

One membership row, carrying enough of the member's identity that the person reading it can tell who it is.

This used to be organization_id, user_id, role and created_at alone, which meant console's members table could only render a raw uuid -- an organization owner literally could not tell who their own collaborators were (console/components/members/member-cell.tsx, and console/openapi/PENDING.md, both said so). Owner ruling 2026-09-09 item 8: name, email and profile picture, all three, visible to fellow members.

Three shape decisions, each with a precedent in this workspace:

  • Denormalized onto the membership row, not a separate identity lookup. A lookup route would have to answer for a user_id in isolation, which is a user-enumeration oracle by construction: it cannot know the caller shares an organization with the subject without being handed the organization, at which point it is this route with extra steps. Resolving at response time is what kernel_http_v2_admin::contracts::organizations already does.
  • email is Option, following UserSummaryResponse: an account with no reachable address is a real state (see UserEmailService), not a failed lookup, and a member list must not 500 because one member signed up through a provider that returned no address.
  • avatar_asset_id, not an avatar_url. The /v2 rule is asset ids on the wire, addressed through /api/v2/assets/{id} by the consumer (kernel_http_v2_reseller::contracts::reseller, kernel_http_v2_catalog::contracts::products's icon_asset_id). /v1 baked /api/v1/assets/{id}?v={updated_at} into responses and pinned the wire to a path /v2 deletes. It also matters here for a second reason: an id is an opaque kernel uuid, so no part of this field is derived from the member's email address the way a Gravatar-style hash or an OAuth provider's CDN URL would be. The provider's avatar is downloaded into our own object storage at login (AvatarImportService), so a member's picture never sends a viewer to a third party.
One membership row, carrying enough of the member's identity that the person reading it can tell **who** it is. This used to be `organization_id`, `user_id`, `role` and `created_at` alone, which meant console's members table could only render a raw uuid -- an organization owner literally could not tell who their own collaborators were (`console/components/members/member-cell.tsx`, and `console/openapi/PENDING.md`, both said so). Owner ruling 2026-09-09 item 8: name, email **and** profile picture, all three, visible to fellow members. Three shape decisions, each with a precedent in this workspace: - **Denormalized onto the membership row, not a separate identity lookup.** A lookup route would have to answer for a `user_id` in isolation, which is a user-enumeration oracle by construction: it cannot know the caller shares an organization with the subject without being handed the organization, at which point it is this route with extra steps. Resolving at response time is what `kernel_http_v2_admin::contracts::organizations` already does. - **`email` is `Option`**, following `UserSummaryResponse`: an account with no reachable address is a real state (see [`UserEmailService`](kernel_core::application::services::users::user_email_service::UserEmailService)), not a failed lookup, and a member list must not 500 because one member signed up through a provider that returned no address. - **`avatar_asset_id`, not an `avatar_url`.** The `/v2` rule is asset ids on the wire, addressed through `/api/v2/assets/{id}` by the consumer (`kernel_http_v2_reseller::contracts::reseller`, `kernel_http_v2_catalog::contracts::products`'s `icon_asset_id`). `/v1` baked `/api/v1/assets/{id}?v={updated_at}` into responses and pinned the wire to a path `/v2` deletes. It also matters here for a second reason: an id is an opaque kernel uuid, so no part of this field is derived from the member's email address the way a Gravatar-style hash or an OAuth provider's CDN URL would be. The provider's avatar is downloaded into our own object storage at login (`AvatarImportService`), so a member's picture never sends a viewer to a third party.
created_at
​string · date-time · required
name
​string · required

The member's display name.

organization_id
​string · uuid · required
role
​OrganizationRoleResponse · enum · required
Enum values:
owner
admin
billing
member
user_id
​string · uuid · required
avatar_asset_id
​string | null · uuid

The member's profile picture, addressed through /api/v2/assets/{id}. None when they never set one.

email
​string | null

None when the account has no reachable address.


List Organization Audit Log

GET
/api/v2/organizations/{id}/audit

Lists the organization's activity, most recent first. Only entries a caller explicitly scoped to a customer organization appear here: platform-internal events are excluded structurally, not by filtering. Narrow to one project with project_id, or to one action name with action.

List Organization Audit Log › path Parameters

id
​string · uuid · required · style: simple

List Organization Audit Log › query Parameters

action
​string | null · style: form

Exact match on the action name, e.g. subscription.activated.

Exact match on the action name, e.g. subscription.activated.

page
​integer · uint32 · min: 0 · style: form

Page number (1-indexed). Defaults to 1.

Page number (1-indexed). Defaults to 1.

Default: 1
page_size
​integer · uint32 · min: 0 · style: form

Number of items per page. Defaults to 20, max 100.

Number of items per page. Defaults to 20, max 100.

Default: 20
project_id
​string | null · uuid · style: form

Narrows to one project inside the organization. A project always implies its organization, so this never widens what the caller sees.

Narrows to one project inside the organization. A project always implies its organization, so this never widens what the caller sees.

service_instance_id
​string | null · uuid · style: form

Narrow to one service instance: what happened to this server (ZC-1277). Composes with project_id rather than replacing it, and both only ever narrow inside the organization the path already scoped.

Narrow to one service instance: what happened to this server (ZC-1277). Composes with project_id rather than replacing it, and both only ever narrow inside the organization the path already scoped.

Default: null

List Organization Audit Log › Responses

Audit entries

​AuditEntryResponse[] · required
​PaginationMeta · required

Pagination metadata included in paginated responses


List Organization Domains

GET
/api/v2/organizations/{id}/domains

Lists every domain the organization has claimed, proven or not. Each row carries the TXT challenge to publish and the outcome of the most recent check.

List Organization Domains › path Parameters

id
​string · uuid · required · style: simple

List Organization Domains › Responses

Domain verifications

What the organization must publish, and the state of that proof so far. Returned by `GET /organizations/{id}/domains`, `POST /organizations/{id}/domains` (creating or re-reading the challenge), and `POST .../domains/{domain}/check` (after running it). Deliberately carries no `id`: the resource this API exposes is keyed by `(organization, domain)`, never by a bare row id a caller could try against a different organization's scope. Mirrors the fields the design doc's Domains-tab table needs (Domain, Status, Detail, Last checked) plus the record itself, plus the three fields the four-state machine added that no consumer could render at all before this: `first_verified_at` and `lost_since` are what state 6 ("Lost, inside grace" -- "will stop being served on 18 September") is computed from, and `grandfathered_until` is the date the "four live production domains" migration note says the tab must show its affected customers. `grandfathered_until` is populated only by that one migration and is genuinely a customer-facing deadline (not an internal implementation detail), which is why it is here rather than withheld: withholding it would leave console unable to render the exact screen the design section that created it requires. There is no `created_at`/`updated_at` here: a freshly issued challenge cannot honestly report either.
domain
​string · required
record_name
​string · required
record_type
​string · required

Always TXT. Present so a client never has to hardcode the record type it renders next to record_name/record_value.

record_value
​string · required
status
​string · required

pending, verified, lost, or failed.

first_verified_at
​string | null · date-time

The first time this row was ever proven, set once and never moved again even through a later Lost/Verified cycle. What a "Verified on 1 March" message (design doc state 4/5) reads from; distinct from verified_at, which keeps advancing on every successful recheck.

grandfathered_until
​string | null · date-time

A dated exemption for one of the handful of production domains that were serving before this feature existed (design doc, "The four live production domains"). Some only for those rows, and only until the date itself: after it, an unproven domain in this state stops being served. Absent for every row created through the ordinary start flow.

last_checked_at
​string | null · date-time
last_error
​string | null

Kernel's own sentence for the most recent refusal, verbatim, per the design doc's "what kernel returns" (kernel writes the sentence, console renders it as-is).

lost_since
​string | null · date-time

When a Verified row's proof was most recently found gone or replaced. Some only while status is lost. Combined with the domain's fourteen-day grace period, this is what lets a client compute the exact date state 6 names ("will stop being served on lost_since + 14 days").

verified_at
​string | null · date-time

Add Organization Domain

POST
/api/v2/organizations/{id}/domains

Starts proving the organization controls a domain, or re-reads the challenge if it already started one. Publish a TXT record at the returned name with the returned value, then call the check route (or wait for the automatic recheck).

Add Organization Domain › path Parameters

id
​string · uuid · required · style: simple

Add Organization Domain › Request Body

Body for `POST /organizations/{id}/domains`.
domain
​string · required

The hostname the organization is proving it controls, for example shop.acme.com. Normalized (lowercased, trailing root dot stripped) and validated server-side; see the domain's own normalize_domain.

Add Organization Domain › Responses

The challenge to publish

What the organization must publish, and the state of that proof so far. Returned by `GET /organizations/{id}/domains`, `POST /organizations/{id}/domains` (creating or re-reading the challenge), and `POST .../domains/{domain}/check` (after running it). Deliberately carries no `id`: the resource this API exposes is keyed by `(organization, domain)`, never by a bare row id a caller could try against a different organization's scope. Mirrors the fields the design doc's Domains-tab table needs (Domain, Status, Detail, Last checked) plus the record itself, plus the three fields the four-state machine added that no consumer could render at all before this: `first_verified_at` and `lost_since` are what state 6 ("Lost, inside grace" -- "will stop being served on 18 September") is computed from, and `grandfathered_until` is the date the "four live production domains" migration note says the tab must show its affected customers. `grandfathered_until` is populated only by that one migration and is genuinely a customer-facing deadline (not an internal implementation detail), which is why it is here rather than withheld: withholding it would leave console unable to render the exact screen the design section that created it requires. There is no `created_at`/`updated_at` here: a freshly issued challenge cannot honestly report either.
domain
​string · required
record_name
​string · required
record_type
​string · required

Always TXT. Present so a client never has to hardcode the record type it renders next to record_name/record_value.

record_value
​string · required
status
​string · required

pending, verified, lost, or failed.

first_verified_at
​string | null · date-time

The first time this row was ever proven, set once and never moved again even through a later Lost/Verified cycle. What a "Verified on 1 March" message (design doc state 4/5) reads from; distinct from verified_at, which keeps advancing on every successful recheck.

grandfathered_until
​string | null · date-time

A dated exemption for one of the handful of production domains that were serving before this feature existed (design doc, "The four live production domains"). Some only for those rows, and only until the date itself: after it, an unproven domain in this state stops being served. Absent for every row created through the ordinary start flow.

last_checked_at
​string | null · date-time
last_error
​string | null

Kernel's own sentence for the most recent refusal, verbatim, per the design doc's "what kernel returns" (kernel writes the sentence, console renders it as-is).

lost_since
​string | null · date-time

When a Verified row's proof was most recently found gone or replaced. Some only while status is lost. Combined with the domain's fourteen-day grace period, this is what lets a client compute the exact date state 6 names ("will stop being served on lost_since + 14 days").

verified_at
​string | null · date-time

Check Organization Domain

POST
/api/v2/organizations/{id}/domains/{domain}/check

Runs the TXT challenge for one of the organization's domains right now and persists the outcome, instead of waiting for the automatic recheck. Tightly rate-limited: this performs a real DNS lookup for the named hostname.

Check Organization Domain › path Parameters

domain
​string · required · style: simple
id
​string · uuid · required · style: simple

Check Organization Domain › Responses

The verification after running the check

What the organization must publish, and the state of that proof so far. Returned by `GET /organizations/{id}/domains`, `POST /organizations/{id}/domains` (creating or re-reading the challenge), and `POST .../domains/{domain}/check` (after running it). Deliberately carries no `id`: the resource this API exposes is keyed by `(organization, domain)`, never by a bare row id a caller could try against a different organization's scope. Mirrors the fields the design doc's Domains-tab table needs (Domain, Status, Detail, Last checked) plus the record itself, plus the three fields the four-state machine added that no consumer could render at all before this: `first_verified_at` and `lost_since` are what state 6 ("Lost, inside grace" -- "will stop being served on 18 September") is computed from, and `grandfathered_until` is the date the "four live production domains" migration note says the tab must show its affected customers. `grandfathered_until` is populated only by that one migration and is genuinely a customer-facing deadline (not an internal implementation detail), which is why it is here rather than withheld: withholding it would leave console unable to render the exact screen the design section that created it requires. There is no `created_at`/`updated_at` here: a freshly issued challenge cannot honestly report either.
domain
​string · required
record_name
​string · required
record_type
​string · required

Always TXT. Present so a client never has to hardcode the record type it renders next to record_name/record_value.

record_value
​string · required
status
​string · required

pending, verified, lost, or failed.

first_verified_at
​string | null · date-time

The first time this row was ever proven, set once and never moved again even through a later Lost/Verified cycle. What a "Verified on 1 March" message (design doc state 4/5) reads from; distinct from verified_at, which keeps advancing on every successful recheck.

grandfathered_until
​string | null · date-time

A dated exemption for one of the handful of production domains that were serving before this feature existed (design doc, "The four live production domains"). Some only for those rows, and only until the date itself: after it, an unproven domain in this state stops being served. Absent for every row created through the ordinary start flow.

last_checked_at
​string | null · date-time
last_error
​string | null

Kernel's own sentence for the most recent refusal, verbatim, per the design doc's "what kernel returns" (kernel writes the sentence, console renders it as-is).

lost_since
​string | null · date-time

When a Verified row's proof was most recently found gone or replaced. Some only while status is lost. Combined with the domain's fourteen-day grace period, this is what lets a client compute the exact date state 6 names ("will stop being served on lost_since + 14 days").

verified_at
​string | null · date-time

Remove Organization Domain

DELETE
/api/v2/organizations/{id}/domains/{domain}

Removes the organization's own claim on a domain, freeing the name for anyone (including this organization) to claim again. Refused while any of the organization's own service instances is currently exposed on this domain -- detach those first; the error response names them.

Remove Organization Domain › path Parameters

domain
​string · required · style: simple
id
​string · uuid · required · style: simple

Remove Organization Domain › Responses

Removed

No data returned

List Organization API Tokens

GET
/api/v2/organizations/{id}/api-tokens

Lists the live API tokens this organization owns, newest first, with each token's lifetime shape, capability set and last use. Revoked tokens are omitted: this is a credential inventory, not an audit log. No response here ever carries a secret, and the secret shown once by the create call cannot be recovered from this route or any other. Requires an Owner or Admin membership, or being the organization's personal owner: a credential inventory says which automation exists and what it may do, which is not the same question as who belongs to the organization.

List Organization API Tokens › path Parameters

id
​string · uuid · required · style: simple

List Organization API Tokens › Responses

The organization's live API tokens


Create Organization API Token

POST
/api/v2/organizations/{id}/api-tokens

Mints an API token owned by this organization and returns the secret exactly once, in the form zaroz_pat_{tokenId}_{secret}. Only a digest of the secret half is stored, so a caller that loses this response mints a replacement rather than recovering this one; no list or detail route can return it again.

lifetime chooses which of the two shapes the token has, and OMITTING IT YIELDS THE SAFER ONE. membership_tied (the default) is revoked automatically when the creator's membership of this organization ends. organization_owned belongs to the organization and keeps working afterwards, which is the point of an organization token and also the reason it is never what a caller gets by accident.

capabilities may be empty, and an empty set authorizes nothing: for an organization credential, absence denies rather than permits. Every capability is granted scoped to this organization, never globally, and a staff-only capability is refused outright no matter who is asking. Nobody can grant a capability they do not currently hold themselves.

Requires an Owner or Admin membership, or being the organization's personal owner: minting a credential that acts for the organization is the same authority as adding a member to it.

Create Organization API Token › path Parameters

id
​string · uuid · required · style: simple

Create Organization API Token › Request Body

Body of `POST /organizations/{id}/api-tokens`.
name
​string · required
​ApiTokenCapability[]

What the token may do, scoped to this organization. An empty list mints a token that authorizes nothing: for an organization credential, absence denies rather than permits.

Default: []
expires_at
​string | null · date-time

Omit for a token that never expires on its own. Must be in the future.

Default: null
​

Omitting this yields membership_tied, the safer shape: the token is revoked when the creator's membership of this organization ends. Pass organization_owned only when the credential is meant to outlive whoever set it up.

Default: null

Create Organization API Token › Responses

The token, and its secret, shown once

The only response that ever carries the secret.
secret
​string · required

The full credential, shown exactly once. Only a digest of its secret half is stored, so no later request can return this value again.

​OrganizationApiTokenResponse · required

One organization API token. Never carries the secret. The secret exists in exactly one response, [CreatedOrganizationApiTokenResponse], and only its digest is stored, so nothing can put it back here later.


List Grantable Organization API Token Capabilities

GET
/api/v2/organizations/{id}/api-tokens/grantable-capabilities

The capabilities the caller may put in an API token of this organization: every capability they currently hold themselves, minus the staff-only ones, which no organization token may carry. Create refuses exactly the rest, by the same domain rule, so a token screen renders from this list rather than a hardcoded copy. max_lifetime_days is the longest lifetime kernel accepts, or null when there is no maximum. Same access as the other organization token routes: an Owner or Admin membership, or being the organization's personal owner.

List Grantable Organization API Token Capabilities › path Parameters

id
​string · uuid · required · style: simple

List Grantable Organization API Token Capabilities › Responses

The capabilities this caller may grant on this organization

max_lifetime_days
​integer | null · uint32 · min: 0

Longest lifetime kernel accepts for an organization token, in days. null means kernel enforces no maximum: only that expires_at is in the future, and omitting it means the token never expires.


Revoke Organization API Token

DELETE
/api/v2/organizations/{id}/api-tokens/{token_id}

Revokes one of this organization's API tokens immediately. Every other token it holds keeps working, which is what makes rotation an overlap rather than an outage: mint the replacement, deploy it, then revoke this one.

A token id that belongs to another organization, to a person, or to nothing at all all answer the same not found, so this route cannot be used to discover which token ids exist. Revoking an already-revoked token is that same not found. Requires the same Owner or Admin membership that minting does.

Revoke Organization API Token › path Parameters

id
​string · uuid · required · style: simple
token_id
​string · uuid · required · style: simple

Revoke Organization API Token › Responses

Token revoked

No data returned

List Organization Invitations

GET
/api/v2/organizations/{id}/invitations

Lists an organization's currently-pending, unexpired invitations, for the Members tab's pending-invites section. Requires the same bar as inviting a member.

List Organization Invitations › path Parameters

id
​string · uuid · required · style: simple

List Organization Invitations › Responses

Pending invitations

One invitation row, for `GET /organizations/{id}/invitations` (an organization's own pending invites) and for the response to `POST /organizations/{id}/invitations` (the one just sent). There is deliberately no `From<OrganizationInvitation>`, matching `OrganizationMemberResponse`'s own reasoning in `contracts::organizations`: the inviter's name and picture cannot be filled from the invitation row alone, so a bare `From` would be an invitation to serialize a blank inviter without anyone noticing.
created_at
​string · date-time · required
email
​string · required
expires_at
​string · date-time · required
id
​string · uuid · required
invited_by_name
​string · required
organization_id
​string · uuid · required
organization_name
​string · required

Denormalized onto every row (also present, redundantly, on an organization-scoped listing) so GET /me/invitations does not need a second round trip per organization to render a usable list.

role
​OrganizationRoleResponse · enum · required
Enum values:
owner
admin
billing
member
status
​string · enum · required

The invitation's effective status: a stale pending or declined row past its own expiry reads as expired here even before any sweep has rewritten the stored column. See OrganizationInvitation::effective_status.

Enum values:
pending
accepted
declined
revoked
expired
invited_by_avatar_asset_id
​string | null · uuid
organization_icon_asset_id
​string | null · uuid
responded_at
​string | null · date-time

Create Organization Invitation

POST
/api/v2/organizations/{id}/invitations

Invites an email address to join an organization the caller owns or administers. Answers the same 201 shape whether or not the address belongs to an existing account (no enumeration oracle). Refuses with 429 and a machine-readable code when the sender's email is unverified, or any ZC-1346 rate limit or re-invite cool-down applies.

Create Organization Invitation › path Parameters

id
​string · uuid · required · style: simple

Create Organization Invitation › Request Body

Body of `POST /organizations/{id}/invitations`. `role` reuses [`OrganizationRoleRequest`] as-is, including its `Owner` variant, rather than a narrower enum without it: the "an invitation cannot grant the owner role" rule is enforced by `OrganizationInvitation::create` (`OrganizationInvitationValidationError::OwnerRoleNotInvitable`) at the domain layer. Narrowing the wire type here would only make this request's shape quietly diverge from `AddOrganizationMemberRequest`'s for no enforcement benefit. Unlike `AddOrganizationMemberRequest`, there is no default role: an invite sent with no role specified is a validation error at the application layer, not a silent grant of the least-privileged one. Inviting is a rarer, more deliberate action than adding an already-known member, so silently guessing on the caller's behalf is the wrong failure mode here.
email
​string · required
role
​OrganizationRoleRequest · enum · required

Distinct from OrganizationRoleResponse because request and response enums are separate types on /v2 (design doc §43.4), even where they currently carry the same variants.

Enum values:
owner
admin
billing
member

Create Organization Invitation › Responses

The created (or, if one was already pending, existing) invitation

One invitation row, for `GET /organizations/{id}/invitations` (an organization's own pending invites) and for the response to `POST /organizations/{id}/invitations` (the one just sent). There is deliberately no `From<OrganizationInvitation>`, matching `OrganizationMemberResponse`'s own reasoning in `contracts::organizations`: the inviter's name and picture cannot be filled from the invitation row alone, so a bare `From` would be an invitation to serialize a blank inviter without anyone noticing.
created_at
​string · date-time · required
email
​string · required
expires_at
​string · date-time · required
id
​string · uuid · required
invited_by_name
​string · required
organization_id
​string · uuid · required
organization_name
​string · required

Denormalized onto every row (also present, redundantly, on an organization-scoped listing) so GET /me/invitations does not need a second round trip per organization to render a usable list.

role
​OrganizationRoleResponse · enum · required
Enum values:
owner
admin
billing
member
status
​string · enum · required

The invitation's effective status: a stale pending or declined row past its own expiry reads as expired here even before any sweep has rewritten the stored column. See OrganizationInvitation::effective_status.

Enum values:
pending
accepted
declined
revoked
expired
invited_by_avatar_asset_id
​string | null · uuid
organization_icon_asset_id
​string | null · uuid
responded_at
​string | null · date-time

Revoke Organization Invitation

DELETE
/api/v2/organizations/{id}/invitations/{invitation_id}

Withdraws a pending invitation. Requires the same bar as sending one. Refuses if the invitation has already been answered or has expired.

Revoke Organization Invitation › path Parameters

id
​string · uuid · required · style: simple
invitation_id
​string · uuid · required · style: simple

Revoke Organization Invitation › Responses

Revoked

No data returned

List My Invitations

GET
/api/v2/me/invitations

Lists organization invitations addressed to the caller's own VERIFIED email, filtered by status (pending or declined). A caller with no verified email always gets an empty list.

List My Invitations › query Parameters

status
​MyInvitationStatusFilter · enum · required · style: form

Only pending and declined are listable this way, matching the design's route table: accepted, revoked and expired invitations do not belong in a recipient's actionable inbox.

Enum values:
pending
declined

List My Invitations › Responses

The caller's invitations

One invitation row, for `GET /organizations/{id}/invitations` (an organization's own pending invites) and for the response to `POST /organizations/{id}/invitations` (the one just sent). There is deliberately no `From<OrganizationInvitation>`, matching `OrganizationMemberResponse`'s own reasoning in `contracts::organizations`: the inviter's name and picture cannot be filled from the invitation row alone, so a bare `From` would be an invitation to serialize a blank inviter without anyone noticing.
created_at
​string · date-time · required
email
​string · required
expires_at
​string · date-time · required
id
​string · uuid · required
invited_by_name
​string · required
organization_id
​string · uuid · required
organization_name
​string · required

Denormalized onto every row (also present, redundantly, on an organization-scoped listing) so GET /me/invitations does not need a second round trip per organization to render a usable list.

role
​OrganizationRoleResponse · enum · required
Enum values:
owner
admin
billing
member
status
​string · enum · required

The invitation's effective status: a stale pending or declined row past its own expiry reads as expired here even before any sweep has rewritten the stored column. See OrganizationInvitation::effective_status.

Enum values:
pending
accepted
declined
revoked
expired
invited_by_avatar_asset_id
​string | null · uuid
organization_icon_asset_id
​string | null · uuid
responded_at
​string | null · date-time

Get Organization Invitation

GET
/api/v2/invitations/{id}

The review page a recipient opens: organization name and icon, inviter name and avatar, role and expiry. Requires the caller's VERIFIED email to match the invitation's address; a signed-in caller with a different verified email gets a 403 carrying only their own masked address, nothing about the organization.

Get Organization Invitation › path Parameters

id
​string · uuid · required · style: simple

Get Organization Invitation › Responses

The invitation, ready to review

The page a recipient opens for `GET /invitations/{id}`: enough to decide whether to accept, nothing more. No `email` field -- the recipient already knows their own address, and this response is only ever served once the caller's verified email has already been matched against the invitation (an email mismatch is refused with [`MaskedInvitationEmailResponse`] instead, which shows the organization nothing about itself in return).
created_at
​string · date-time · required
expires_at
​string · date-time · required
id
​string · uuid · required
inviter_name
​string · required
organization_id
​string · uuid · required

Where Accept navigates once it succeeds.

organization_name
​string · required
role
​OrganizationRoleResponse · enum · required
Enum values:
owner
admin
billing
member
status
​string · enum · required

The invitation's effective status, not necessarily the raw stored column.

Enum values:
pending
accepted
declined
revoked
expired
inviter_avatar_asset_id
​string | null · uuid
organization_icon_asset_id
​string | null · uuid

Accept Organization Invitation

POST
/api/v2/invitations/{id}/accept

Accepts the invitation and inserts the organization_members row in the same transaction. Never changes an existing member's role: a caller who already belongs to the organization is refused with 409 and the invitation is cancelled. Idempotent on an already-accepted invitation, which answers success and writes nothing. Allowed on a declined, unexpired invitation too (the decline-undo). Requires the caller's VERIFIED email to match; a mismatch gets the same masked-email 403 the review route does.

Accept Organization Invitation › path Parameters

id
​string · uuid · required · style: simple

Accept Organization Invitation › Responses

The now-accepted invitation

The page a recipient opens for `GET /invitations/{id}`: enough to decide whether to accept, nothing more. No `email` field -- the recipient already knows their own address, and this response is only ever served once the caller's verified email has already been matched against the invitation (an email mismatch is refused with [`MaskedInvitationEmailResponse`] instead, which shows the organization nothing about itself in return).
created_at
​string · date-time · required
expires_at
​string · date-time · required
id
​string · uuid · required
inviter_name
​string · required
organization_id
​string · uuid · required

Where Accept navigates once it succeeds.

organization_name
​string · required
role
​OrganizationRoleResponse · enum · required
Enum values:
owner
admin
billing
member
status
​string · enum · required

The invitation's effective status, not necessarily the raw stored column.

Enum values:
pending
accepted
declined
revoked
expired
inviter_avatar_asset_id
​string | null · uuid
organization_icon_asset_id
​string | null · uuid

Decline Organization Invitation

POST
/api/v2/invitations/{id}/decline

Declines the invitation. Starts the ZC-1346 30-day re-invite cool-down for this organization+address, which the recipient can lift early with allow-reinvite, or undo entirely by accepting anyway before the invitation's own expiry. Requires the caller's VERIFIED email to match.

Decline Organization Invitation › path Parameters

id
​string · uuid · required · style: simple

Decline Organization Invitation › Responses

The now-declined invitation

The page a recipient opens for `GET /invitations/{id}`: enough to decide whether to accept, nothing more. No `email` field -- the recipient already knows their own address, and this response is only ever served once the caller's verified email has already been matched against the invitation (an email mismatch is refused with [`MaskedInvitationEmailResponse`] instead, which shows the organization nothing about itself in return).
created_at
​string · date-time · required
expires_at
​string · date-time · required
id
​string · uuid · required
inviter_name
​string · required
organization_id
​string · uuid · required

Where Accept navigates once it succeeds.

organization_name
​string · required
role
​OrganizationRoleResponse · enum · required
Enum values:
owner
admin
billing
member
status
​string · enum · required

The invitation's effective status, not necessarily the raw stored column.

Enum values:
pending
accepted
declined
revoked
expired
inviter_avatar_asset_id
​string | null · uuid
organization_icon_asset_id
​string | null · uuid

Allow Organization Reinvite

POST
/api/v2/invitations/{id}/allow-reinvite

Lifts the ZC-1346 30-day re-invite cool-down early, so the organization may send a fresh invitation to this address right away. Only meaningful on a declined invitation. Requires the caller's VERIFIED email to match.

Allow Organization Reinvite › path Parameters

id
​string · uuid · required · style: simple

Allow Organization Reinvite › Responses

Cool-down lifted

No data returned

List My Projects

GET
/api/v2/projects

Lists the caller's projects, across every organization they belong to. Every row carries its own organization_id, so the listing can be grouped by owner; for one organization's projects alone, read /organizations/{id}/projects instead.

List My Projects › Responses

One project as seen by a member of its organization, on the `/v2` console surface (design doc §34.1). `/v2` DTOs are always named `*Request` / `*Response` -- there is no `ProjectView` here on purpose.
created_at
​string · date-time · required
id
​string · uuid · required
name
​string · required
organization_id
​string · uuid · required
slug
​string · required

URL-safe identifier, unique within the owning organization. Console routes are /{org}/{slug}.

updated_at
​string · date-time · required
description
​string | null

List Organization Projects

GET
/api/v2/organizations/{id}/projects

Lists one organization's projects. Answers 404 for an organization the caller does not belong to, the same way reading that organization directly would, so an organization the caller may not see is indistinguishable from one that does not exist. This can only ever return projects listMyProjects would also have returned.

List Organization Projects › path Parameters

id
​string · uuid · required · style: simple

List Organization Projects › Responses

One project as seen by a member of its organization, on the `/v2` console surface (design doc §34.1). `/v2` DTOs are always named `*Request` / `*Response` -- there is no `ProjectView` here on purpose.
created_at
​string · date-time · required
id
​string · uuid · required
name
​string · required
organization_id
​string · uuid · required
slug
​string · required

URL-safe identifier, unique within the owning organization. Console routes are /{org}/{slug}.

updated_at
​string · date-time · required
description
​string | null

Create Project

POST
/api/v2/organizations/{id}/projects

Creates a project in an organization the caller owns or administers. In a personal organization the slug games is reserved for the gaming dashboard's project: 422 project-slug-reserved.

Create Project › path Parameters

id
​string · uuid · required · style: simple

Create Project › Request Body

Body of `POST /organizations/{id}/projects`.
name
​string · required
slug
​string · required

URL-safe identifier, unique within the owning organization (lowercase ascii letters, digits, and single internal hyphens).

description
​string | null
Default: null

Create Project › Responses

One project as seen by a member of its organization, on the /v2 console surface (design doc §34.1). /v2 DTOs are always named *Request / *Response -- there is no ProjectView here on purpose.

One project as seen by a member of its organization, on the `/v2` console surface (design doc §34.1). `/v2` DTOs are always named `*Request` / `*Response` -- there is no `ProjectView` here on purpose.
created_at
​string · date-time · required
id
​string · uuid · required
name
​string · required
organization_id
​string · uuid · required
slug
​string · required

URL-safe identifier, unique within the owning organization. Console routes are /{org}/{slug}.

updated_at
​string · date-time · required
description
​string | null

List My Project Stats

GET
/api/v2/projects/stats

The resource count and current-cycle spend for every project List My Projects returns, in one request rather than one per card. Same visibility as that operation: a project absent from it is absent here.

List My Project Stats › Responses

The aggregates behind console's project card badges and project overview tiles, for one project. **A separate operation, not fields on [`ProjectResponse`].** A field cannot be streamed apart from the object carrying it: `resource_count` on `ProjectResponse` would make `GET /projects/{id}` -- and every row of `GET /projects` -- wait on an aggregation over `subscriptions` and `invoice_line_items` before the project's own name could be rendered. Split out, console paints the project immediately and fills these in behind its own suspense boundary.
​ProjectCycleSpendResponse[] · required

What the project is being charged for the service it is receiving right now, one entry per currency. Empty when nothing in the project is under a live service window -- which is the honest answer for a project with no subscriptions and for one whose subscriptions have all lapsed, never an error. In practice this holds exactly one entry: the platform transacts in EUR only.

project_id
​string · uuid · required
resource_count
​integer · int64 · required

How many live subscriptions this project holds. A "resource" here is a subscription -- design doc §34.1 puts Project directly above Subscription, so one purchase is one row -- not a resources row (which would count a single VPS as its VM plus each of its volumes) and not a service instance (which would drop a paid subscription still awaiting activation). "Live" is deleted_at IS NULL and nothing more, the same rule that decides whether the project can be deleted and the same rule GET /console/subscriptions?project_id= pages with, so this number is exactly how many rows the caller sees on clicking through.


Get Project By Slug

GET
/api/v2/organizations/{id}/projects/by-slug/{slug}

Gets one project the caller's organization owns, by its slug.

Get Project By Slug › path Parameters

id
​string · uuid · required · style: simple
slug
​string · required · style: simple

Get Project By Slug › Responses

One project as seen by a member of its organization, on the /v2 console surface (design doc §34.1). /v2 DTOs are always named *Request / *Response -- there is no ProjectView here on purpose.

One project as seen by a member of its organization, on the `/v2` console surface (design doc §34.1). `/v2` DTOs are always named `*Request` / `*Response` -- there is no `ProjectView` here on purpose.
created_at
​string · date-time · required
id
​string · uuid · required
name
​string · required
organization_id
​string · uuid · required
slug
​string · required

URL-safe identifier, unique within the owning organization. Console routes are /{org}/{slug}.

updated_at
​string · date-time · required
description
​string | null

Get Project

GET
/api/v2/projects/{id}

Gets one project the caller's organization owns.

Get Project › path Parameters

id
​string · uuid · required · style: simple

Get Project › Responses

One project as seen by a member of its organization, on the /v2 console surface (design doc §34.1). /v2 DTOs are always named *Request / *Response -- there is no ProjectView here on purpose.

One project as seen by a member of its organization, on the `/v2` console surface (design doc §34.1). `/v2` DTOs are always named `*Request` / `*Response` -- there is no `ProjectView` here on purpose.
created_at
​string · date-time · required
id
​string · uuid · required
name
​string · required
organization_id
​string · uuid · required
slug
​string · required

URL-safe identifier, unique within the owning organization. Console routes are /{org}/{slug}.

updated_at
​string · date-time · required
description
​string | null

Delete Project

DELETE
/api/v2/projects/{id}

Deletes a project in an organization the caller owns or administers. Refuses if the project still holds any subscriptions, and for a personal organization's games project, which belongs to the gaming dashboard (403 project-gaming-managed).

Delete Project › path Parameters

id
​string · uuid · required · style: simple

Delete Project › Responses

Deleted

No data returned

Update Project

PATCH
/api/v2/projects/{id}

Renames, re-slugs, and/or re-describes a project in an organization the caller owns or administers. Omitted fields are left unchanged; description: null clears it. A personal organization's games project belongs to the gaming dashboard and cannot be changed here (403 project-gaming-managed), and no project in a personal organization may take the slug games (422 project-slug-reserved).

Update Project › path Parameters

id
​string · uuid · required · style: simple

Update Project › Request Body

Body of `PATCH /projects/{id}`. `name`, `slug`, and `description` are each independently optional, matching PATCH semantics: an absent field leaves it unchanged. `description` is a double option -- absent leaves it unchanged, `null` clears it, and a string sets it -- decoded by `deserialize_double_option`.
description
​string | null
Default: null
name
​string | null
Default: null
slug
​string | null
Default: null

Update Project › Responses

One project as seen by a member of its organization, on the /v2 console surface (design doc §34.1). /v2 DTOs are always named *Request / *Response -- there is no ProjectView here on purpose.

One project as seen by a member of its organization, on the `/v2` console surface (design doc §34.1). `/v2` DTOs are always named `*Request` / `*Response` -- there is no `ProjectView` here on purpose.
created_at
​string · date-time · required
id
​string · uuid · required
name
​string · required
organization_id
​string · uuid · required
slug
​string · required

URL-safe identifier, unique within the owning organization. Console routes are /{org}/{slug}.

updated_at
​string · date-time · required
description
​string | null

Get Project Stats

GET
/api/v2/projects/{id}/stats

The resource count and current-cycle spend for one project. A second read on purpose: these are aggregates over the project's subscriptions and invoice lines, so they are kept off ProjectResponse where they would block the project's own fields. Refused exactly as Get Project is -- a project the caller cannot read is 404, whether or not it exists.

Get Project Stats › path Parameters

id
​string · uuid · required · style: simple

Get Project Stats › Responses

The aggregates behind console's project card badges and project overview tiles, for one project.

A separate operation, not fields on [ProjectResponse]. A field cannot be streamed apart from the object carrying it: resource_count on ProjectResponse would make GET /projects/{id} -- and every row of GET /projects -- wait on an aggregation over subscriptions and invoice_line_items before the project's own name could be rendered. Split out, console paints the project immediately and fills these in behind its own suspense boundary.

The aggregates behind console's project card badges and project overview tiles, for one project. **A separate operation, not fields on [`ProjectResponse`].** A field cannot be streamed apart from the object carrying it: `resource_count` on `ProjectResponse` would make `GET /projects/{id}` -- and every row of `GET /projects` -- wait on an aggregation over `subscriptions` and `invoice_line_items` before the project's own name could be rendered. Split out, console paints the project immediately and fills these in behind its own suspense boundary.
​ProjectCycleSpendResponse[] · required

What the project is being charged for the service it is receiving right now, one entry per currency. Empty when nothing in the project is under a live service window -- which is the honest answer for a project with no subscriptions and for one whose subscriptions have all lapsed, never an error. In practice this holds exactly one entry: the platform transacts in EUR only.

project_id
​string · uuid · required
resource_count
​integer · int64 · required

How many live subscriptions this project holds. A "resource" here is a subscription -- design doc §34.1 puts Project directly above Subscription, so one purchase is one row -- not a resources row (which would count a single VPS as its VM plus each of its volumes) and not a service instance (which would drop a paid subscription still awaiting activation). "Live" is deleted_at IS NULL and nothing more, the same rule that decides whether the project can be deleted and the same rule GET /console/subscriptions?project_id= pages with, so this number is exactly how many rows the caller sees on clicking through.


Get Organization Standing

GET
/api/v2/organizations/{organization_id}/standing

Reads one organization's current standing (ZC-1324 org trust): whether it is restricted, and why. Reachable by any member (Q121) -- the organization's own owner sees the full numeric breakdown (balance, reservations, floor, credit limit, open receivables) in detail; every other member sees only message, the same fixed sentence regardless of the actual reason. restricted is always present. An organization the caller does not belong to answers 404, identical to an invented id.

Get Organization Standing › path Parameters

organization_id
​string · uuid · required · style: simple

Get Organization Standing › Responses

The organization's current standing, at the caller's own disclosure level

`GET /api/v2/organizations/{organization_id}/standing`'s response (`Q121`, decided 2026-09-26): the organization's own owner (and staff, in admin) sees [`StandingResponse::detail`] in full; every other member sees only [`Self::message`], the same fixed sentence for every restriction reason -- naming which fact failed, or which other organization is involved, is exactly the detail `Q121` says is wrong for a co-member to learn. Both fields are `None` for a caller who is not restricted and not the owner: there is nothing to disclose either way.
restricted
​boolean · required
​
message
​string | null