Endpoints

Organizations

GET/api/v1/organizations

List all organizations user is member of.

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Response Body

application/json

curl -X GET "https://example.com/api/v1/organizations"
[  {    "name": "string",    "slug": "string",    "logo_url": "string",    "timezone": "UTC",    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",    "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",    "data_region": "string",    "zitadel_org_id": "string",    "created_at": "2019-08-24T14:15:22Z",    "updated_at": "2019-08-24T14:15:22Z",    "user_role": "owner",    "member_count": 0,    "features": [],    "user_permissions": []  }]
POST/api/v1/organizations

Create new organization with current user as owner. Also provisions a matching Zitadel org.

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Schema for creating an organization.

Response Body

application/json

application/json

curl -X POST "https://example.com/api/v1/organizations" \  -H "Content-Type: application/json" \  -d '{    "name": "string",    "slug": "string"  }'
{  "name": "string",  "slug": "string",  "logo_url": "string",  "timezone": "UTC",  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",  "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",  "data_region": "string",  "zitadel_org_id": "string",  "created_at": "2019-08-24T14:15:22Z",  "updated_at": "2019-08-24T14:15:22Z"}
GET/api/v1/organizations/invitations/pending

List pending invitations for current user.

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Response Body

application/json

curl -X GET "https://example.com/api/v1/organizations/invitations/pending"
[  {    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",    "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",    "email": "string",    "role": "owner",    "token": "string",    "invited_by": "0fdabe0c-eb7c-440a-96d2-2c65906c3777",    "status": "pending",    "created_at": "2019-08-24T14:15:22Z",    "expires_at": "2019-08-24T14:15:22Z",    "is_expired": true,    "is_valid": true,    "organization_name": "string",    "inviter_name": "string",    "inviter_email": "string"  }]
POST/api/v1/organizations/invitations/accept

Accept invitation and join organization.

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Schema for accepting an invitation.

Response Body

application/json

application/json

curl -X POST "https://example.com/api/v1/organizations/invitations/accept" \  -H "Content-Type: application/json" \  -d '{    "token": "string"  }'
{  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",  "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",  "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",  "role": "owner",  "invited_by": "0fdabe0c-eb7c-440a-96d2-2c65906c3777",  "joined_at": "2019-08-24T14:15:22Z",  "created_at": "2019-08-24T14:15:22Z",  "user_email": "string",  "user_name": "string",  "user_avatar_url": "string",  "org_slug": "string"}
GET/api/v1/me/org

Get organization details.

Authorization

APIKeyHeader
X-API-Key<token>

In: header

Response Body

application/json

curl -X GET "https://example.com/api/v1/me/org"
{  "name": "string",  "slug": "string",  "logo_url": "string",  "timezone": "UTC",  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",  "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",  "data_region": "string",  "zitadel_org_id": "string",  "created_at": "2019-08-24T14:15:22Z",  "updated_at": "2019-08-24T14:15:22Z",  "user_role": "owner",  "member_count": 0,  "features": [],  "user_permissions": []}
PATCH/api/v1/me/org

Update organization (owner only).

The actor is resolved rather than required. This route is gated by require_organization_write, which accepts an API key carrying organization:write as well as a member bearer token, so demanding get_current_user here would 403 every machine caller that the scope check had just authorized (issue #603 did exactly that). get_audit_actor (app/dependencies/audit_actor.py) resolves either credential without requiring one, unlike get_optional_user, which still 403s on an API-key-only request because it depends on the same eager HTTPBearer(auto_error=True). The four other audited routes in this module are gated by require_owner / require_admin_or_above, which are bearer-only, so a required user is correct there and is left alone.

AT §4.1's envelope has actor_type = "api_key" for precisely this case, so a machine call is recorded as the key that made it, not as a null user.

Authorization

X-API-Key<token>

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Schema for updating organization.

Deliberately excludes data_region — it is set once at creation and is immutable afterward (TELEMETRY_INGESTION_ARCHITECTURE.md §7.2). extra="forbid" below ensures a data_region field in the request body is rejected (422) rather than silently ignored.

Response Body

application/json

application/json

curl -X PATCH "https://example.com/api/v1/me/org" \  -H "Content-Type: application/json" \  -d '{}'
{  "name": "string",  "slug": "string",  "logo_url": "string",  "timezone": "UTC",  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",  "owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",  "data_region": "string",  "zitadel_org_id": "string",  "created_at": "2019-08-24T14:15:22Z",  "updated_at": "2019-08-24T14:15:22Z"}
DELETE/api/v1/me/org

Delete organization (owner only).

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Response Body

curl -X DELETE "https://example.com/api/v1/me/org"
Empty
GET/api/v1/me/members

List organization members.

Authorization

APIKeyHeader
X-API-Key<token>

In: header

Response Body

application/json

curl -X GET "https://example.com/api/v1/me/members"
[  {    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",    "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",    "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",    "role": "owner",    "invited_by": "0fdabe0c-eb7c-440a-96d2-2c65906c3777",    "joined_at": "2019-08-24T14:15:22Z",    "created_at": "2019-08-24T14:15:22Z",    "user_email": "string",    "user_name": "string",    "user_avatar_url": "string",    "org_slug": "string"  }]
POST/api/v1/me/members/invite

Invite a user to this organization.

If a Zitadel org is provisioned:

  • Looks up the user in Zitadel by email
  • If found: adds them as an org member in Zitadel immediately
  • If not found: creates a Zitadel user (triggers init email) and adds as member

Also creates a UP0 invitation record for backward compatibility.

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Schema for creating an invitation.

Response Body

application/json

application/json

curl -X POST "https://example.com/api/v1/me/members/invite" \  -H "Content-Type: application/json" \  -d '{    "email": "string"  }'
{  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",  "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",  "email": "string",  "role": "owner",  "token": "string",  "invited_by": "0fdabe0c-eb7c-440a-96d2-2c65906c3777",  "status": "pending",  "created_at": "2019-08-24T14:15:22Z",  "expires_at": "2019-08-24T14:15:22Z",  "is_expired": true,  "is_valid": true,  "organization_name": "string",  "inviter_name": "string",  "inviter_email": "string"}
DELETE/api/v1/me/members/{user_id}

Remove member from organization (admin or above). Removes from Zitadel org too.

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Path Parameters

user_id*User Id
Formatuuid

Response Body

application/json

curl -X DELETE "https://example.com/api/v1/me/members/497f6eca-6276-4993-bfeb-53cbbbba6f08"
Empty
PATCH/api/v1/me/members/{user_id}/role

Update a member's role (owner only).

Two request bodies (app.schemas.organization.MemberRoleUpdate):

  • role_id (issue #597, RBAC §3.5/§8) — binds the member to any role visible to the org (system, this org's custom roles, or an entitled plan role), via RoleService.assign_role_to_member, which enforces invariant 1 (refuses to downgrade the last Owner) and invariant 3 (only a current Owner may raise a member to Owner — checked independently of this endpoint's own require_owner gate).
  • role (legacy) — the original four-value enum path, unchanged.

Both refuse to downgrade the last owner away from OWNER, preventing orphaned organizations.

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Path Parameters

user_id*User Id
Formatuuid

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Request body for updating a member's role.

role_id is the RBAC §3.5 / §8 role-assignment path (issue #597) — binds the member to any role visible to the org (system, its own custom roles, or an entitled plan role), including one the legacy four-value role enum cannot represent. When set, it takes priority over role and the legacy enum-only path is not exercised. role stays required when role_id is omitted, so every existing caller (console, CLI, tests) keeps working unchanged.

Response Body

application/json

application/json

curl -X PATCH "https://example.com/api/v1/me/members/497f6eca-6276-4993-bfeb-53cbbbba6f08/role" \  -H "Content-Type: application/json" \  -d '{}'
{  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",  "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",  "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5",  "role": "owner",  "invited_by": "0fdabe0c-eb7c-440a-96d2-2c65906c3777",  "joined_at": "2019-08-24T14:15:22Z",  "created_at": "2019-08-24T14:15:22Z",  "user_email": "string",  "user_name": "string",  "user_avatar_url": "string",  "org_slug": "string"}
GET/api/v1/me/invitations

List pending invitations for organization (admin or above).

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Response Body

application/json

curl -X GET "https://example.com/api/v1/me/invitations"
[  {    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",    "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",    "email": "string",    "role": "owner",    "token": "string",    "invited_by": "0fdabe0c-eb7c-440a-96d2-2c65906c3777",    "status": "pending",    "created_at": "2019-08-24T14:15:22Z",    "expires_at": "2019-08-24T14:15:22Z",    "is_expired": true,    "is_valid": true,    "organization_name": "string",    "inviter_name": "string",    "inviter_email": "string"  }]
DELETE/api/v1/me/invitations/{invitation_id}

Revoke invitation (admin or above).

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Path Parameters

invitation_id*Invitation Id
Formatuuid

Response Body

application/json

curl -X DELETE "https://example.com/api/v1/me/invitations/497f6eca-6276-4993-bfeb-53cbbbba6f08"
Empty
GET/api/v1/me/api-keys

List active API keys for the organization. Owner only.

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Response Body

application/json

curl -X GET "https://example.com/api/v1/me/api-keys"
[  {    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",    "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",    "name": "string",    "key_prefix": "string",    "scopes": [      "string"    ],    "role_id": "ac4e70c8-d5be-48af-93eb-760f58fc91a9",    "is_active": true,    "expires_at": "2019-08-24T14:15:22Z",    "last_used_at": "2019-08-24T14:15:22Z",    "created_at": "2019-08-24T14:15:22Z"  }]
POST/api/v1/me/api-keys

Create an API key for the organization. Owner only.

⚠️ The full key is returned once in the response field key. Store it securely — it cannot be retrieved again.

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Request body to create an API key.

role_id binds the key to an RBAC role (RBAC §3.6, issue #598) — reads prefer it over scopes once set. Optional during the transition window: scopes remains the legacy, still-supported vocabulary for a key that doesn't set it.

Response Body

application/json

application/json

curl -X POST "https://example.com/api/v1/me/api-keys" \  -H "Content-Type: application/json" \  -d '{    "name": "string",    "scopes": [      "string"    ]  }'
{  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",  "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",  "name": "string",  "key_prefix": "string",  "scopes": [    "string"  ],  "role_id": "ac4e70c8-d5be-48af-93eb-760f58fc91a9",  "is_active": true,  "expires_at": "2019-08-24T14:15:22Z",  "last_used_at": "2019-08-24T14:15:22Z",  "created_at": "2019-08-24T14:15:22Z",  "key": "string"}
DELETE/api/v1/me/api-keys/{key_id}

Revoke an API key. Owner only.

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Path Parameters

key_id*Key Id
Formatuuid

Response Body

application/json

curl -X DELETE "https://example.com/api/v1/me/api-keys/497f6eca-6276-4993-bfeb-53cbbbba6f08"
Empty
GET/api/v1/me/service-accounts

List the active service accounts for this org. Metadata only — no secret; a client_secret is never reproducible after creation.

require_admin_or_above, not require_owner like creation and rotation below: an Admin building a GitOps Project needs to see which accounts exist in order to bind one (#567's Add Project picker, GITOPS_ARCHITECTURE.md §7.4), even though only an Owner may mint a new one. Binding an already-authorized identity is a narrower action than creating one.

#562 and #567 each added a GET here independently, with different gates and response models. They are one route: this one. The wider ServiceAccountResponse is kept because the /settings/service-accounts page reads revoked_at, which the picker's narrower shape omitted.

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Response Body

application/json

curl -X GET "https://example.com/api/v1/me/service-accounts"
[  {    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",    "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",    "zitadel_user_id": "string",    "name": "string",    "role": "owner",    "created_at": "2019-08-24T14:15:22Z",    "revoked_at": "2019-08-24T14:15:22Z"  }]
POST/api/v1/me/service-accounts

Create a Zitadel machine user (service account) for this org.

Use this for CLI tools, agents, and packages that need to authenticate against UP0 from private environments (up0 auth login --client-id --client-secret, GITOPS_ARCHITECTURE.md §3.2/§3.3). The client secret returned is shown once.

Owner only. Per GITOPS_ARCHITECTURE.md §2/§3.2, this is the only way a service account is ever created — there is no public signup or bootstrap endpoint. The first service account for an org is necessarily created this way, by an already-logged-in owner on the console's /settings/service-accounts page; up0 service-accounts create (#562) calls this same endpoint once an authenticated caller already exists.

Also binds the new machine user to this org in the UP0-side service_accounts table (issue #561, fix for #489) — without this row the machine user could authenticate to Zitadel but not to UP0's own API, since get_org_context resolves a machine subject's org from this table, not from Zitadel. If the DB write fails, the Zitadel-side machine user is torn down rather than left orphaned and unusable.

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Request body to create a Zitadel machine user (service account).

role is the console/CLI role picker (issue #562): only the four system roles, since no custom-role authoring surface exists yet (CUSTOMIZABLE_RBAC_ACL_ARCHITECTURE.md §3.3, §7). Required, with no default (issue #598, mirroring gitops-sync #564's required service-account binding): an implicit default is how a machine subject ends up with more authority than anyone chose. Editor — a sync agent needs to write Monitor/StatusPage/IngestionToken/Workflow, and cannot delete the org or touch billing (MULTI_TENANCY_AUTHORIZATION_ARCHITECTURE.md §3.2's OWNER-exclusive actions) — is the commonly-right choice, but the caller must say so explicitly. The endpoint resolves this to a role_id before persisting (issue #864 — service_accounts stores role_id, not this enum).

Response Body

application/json

application/json

curl -X POST "https://example.com/api/v1/me/service-accounts" \  -H "Content-Type: application/json" \  -d '{    "name": "string",    "username": "string",    "role": "owner"  }'
{  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",  "machine_user_id": "string",  "username": "string",  "name": "string",  "role": "owner",  "client_id": "string",  "client_secret": "string",  "message": "Save this secret now. You cannot retrieve it again."}
POST/api/v1/me/service-accounts/{service_account_id}/rotate

Rotate a service account's client secret. Owner only.

Invalidates the prior secret immediately (Zitadel holds one active secret per machine user — see zitadel_admin.rotate_machine_secret). The new secret is shown once.

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Path Parameters

service_account_id*Service Account Id
Formatuuid

Response Body

application/json

application/json

curl -X POST "https://example.com/api/v1/me/service-accounts/497f6eca-6276-4993-bfeb-53cbbbba6f08/rotate"
{  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",  "client_id": "string",  "client_secret": "string",  "message": "Save this secret now. You cannot retrieve it again."}
DELETE/api/v1/me/service-accounts/{service_account_id}

Revoke a service account. Owner only.

Marks it revoked in service_accounts (checked by get_current_principal on every subsequent request — see app/dependencies/zitadel_auth.py) and deletes the Zitadel-side machine user, so it can no longer be issued new client_credentials tokens either.

DB-first ordering: the row is marked revoked before the Zitadel call is attempted, so a second request against an already-revoked account 404s from service.revoke()'s own None return rather than re-attempting the Zitadel delete (code review on #562/#864 confirmed this idempotency).

Authorization

HTTPBearer
AuthorizationBearer <token>

In: header

Path Parameters

service_account_id*Service Account Id
Formatuuid

Response Body

application/json

curl -X DELETE "https://example.com/api/v1/me/service-accounts/497f6eca-6276-4993-bfeb-53cbbbba6f08"
Empty