Koo Docs
View as Markdown

Accounts

The account audit log, newest first

GET/accounts/{accountId}/events
Rolesreadereditoradminowner
Parameters
cursorstringquery

Opaque cursor (page.nextCursor).

limitnumberquery

Page size (1–100, default 20).

accountIdstringpathrequired
Returns
200 · AccountEventsPageDto
dataobject[]read-only
pageobjectread-only
cURL
GET /accounts/{accountId}/events
bash
curl https://api.koo.io/accounts/:accountId/events \  -H "Authorization: Bearer kc_your_api_token"
Response
json
{  "data": [    {      "id": "acct_01example0000000000000000x",      "action": "example",      "target": "example",      "targetName": "example",      "status": "ok",      "actorEmail": "dev@example.com",      "actorHandle": "example",      "actorServiceAccountId": "actorserviceaccount_01example0000000000000000x",      "viaSupport": true,      "scope": {        "account": {          "id": "acct_01example0000000000000000x",          "handle": "alice"        },        "project": {          "id": "acct_01example0000000000000000x",          "name": "my-app"        },        "environment": {          "id": "acct_01example0000000000000000x",          "name": "my-app"        },        "services": [          {            "id": "acct_01example0000000000000000x",            "name": "my-app"          }        ]      },      "createdAt": "2026-01-01T00:00:00.000Z",      "before": {        "key": "example"      },      "after": {        "key": "example"      }    }  ],  "page": {    "nextCursor": "example",    "hasMore": true  }}

Create a second account (payment-gated)

POST/accounts

Signed-in humans only, captcha-gated (`X-Turnstile-Token`). The account is created in `payment_pending`: it owns a handle and your owner membership, and **no hosting environment is provisioned**. Every write to it is refused with `402` until a subscription is paid for — that payment is what creates its Control Plane organization. You may hold only one unpaid account at a time. Secondary accounts get **no** always-free allowance, so they are billed from the first service.

Request body
handlestringrequired

Global-unique handle for the new account — the URL/CLI slug.

displayNamestring

Optional pretty name, stored on `profile.displayName`. UIs fall back to the handle.

Returns
201 · AccountDto400 · ErrorEnvelopeDto403 · ErrorEnvelopeDto409 · ErrorEnvelopeDto
idstringread-only

Account identifier.

handlestringrequired

Global-unique account handle — the URL/CLI slug.

tier"Free" | "Plus" | "Pro" | "Enterprise"read-only

The account’s plan, by display name. The internal Free sub-tiers all report `Free`.

status"active" | "suspended" | "pending_deletion" | "provisioning_pending" | "payment_pending"read-only

Account lifecycle status. `provisioning_pending`: the hosting environment is still being set up — deploys are paused until provisioning completes (retry via POST /accounts/:id/reprovision). `payment_pending`: a secondary account that has never been paid for — it has no hosting environment and every write is refused with 402 until a subscription starts.

deleteAtstring <date-time>read-only

Scheduled hard-delete time; set iff `status === 'pending_deletion'`.

isPrimarybooleanread-only

Whether this is the user's primary account (created during onboarding).

onboardingStateobjectread-only

The account's onboarding state machine position.

profileobjectrequired

Editable account profile (avatar, banner, bio, location, links).

countsobjectread-only

Denormalized social/app counts for this account.

createdAtstring <date-time>read-only

Account creation timestamp.

cURL
POST /accounts
bash
curl https://api.koo.io/accounts \  -X POST \  -H "Authorization: Bearer kc_your_api_token" \  -H "Content-Type: application/json" \  -d '{      "handle": "alice"    }'
Response
json
{  "id": "acct_01example0000000000000000x",  "handle": "alice",  "tier": "Free",  "status": "active",  "deleteAt": "2026-01-01T00:00:00.000Z",  "isPrimary": true,  "onboardingState": {    "step": "SIGNED_IN",    "cliDeploy": {      "projectId": "project_01example0000000000000000x",      "environmentId": "environment_01example0000000000000000x",      "serviceId": "svc_01example0000000000000000x"    }  },  "profile": {    "displayName": "My app",    "avatarUrl": "https://example.com",    "bannerUrl": "https://example.com",    "bio": "example",    "location": "example",    "links": {      "key": "example"    }  },  "counts": {    "followers": 0,    "following": 0,    "apps": 0  },  "createdAt": "2026-01-01T00:00:00.000Z"}

Get an account

GET/accounts/{accountId}
Rolesreadereditoradminowner
Parameters
accountIdstringpathrequired
Returns
200 · AccountDto
idstringread-only

Account identifier.

handlestringrequired

Global-unique account handle — the URL/CLI slug.

tier"Free" | "Plus" | "Pro" | "Enterprise"read-only

The account’s plan, by display name. The internal Free sub-tiers all report `Free`.

status"active" | "suspended" | "pending_deletion" | "provisioning_pending" | "payment_pending"read-only

Account lifecycle status. `provisioning_pending`: the hosting environment is still being set up — deploys are paused until provisioning completes (retry via POST /accounts/:id/reprovision). `payment_pending`: a secondary account that has never been paid for — it has no hosting environment and every write is refused with 402 until a subscription starts.

deleteAtstring <date-time>read-only

Scheduled hard-delete time; set iff `status === 'pending_deletion'`.

isPrimarybooleanread-only

Whether this is the user's primary account (created during onboarding).

onboardingStateobjectread-only

The account's onboarding state machine position.

profileobjectrequired

Editable account profile (avatar, banner, bio, location, links).

countsobjectread-only

Denormalized social/app counts for this account.

createdAtstring <date-time>read-only

Account creation timestamp.

cURL
GET /accounts/{accountId}
bash
curl https://api.koo.io/accounts/:accountId \  -H "Authorization: Bearer kc_your_api_token"
Response
json
{  "id": "acct_01example0000000000000000x",  "handle": "alice",  "tier": "Free",  "status": "active",  "deleteAt": "2026-01-01T00:00:00.000Z",  "isPrimary": true,  "onboardingState": {    "step": "SIGNED_IN",    "cliDeploy": {      "projectId": "project_01example0000000000000000x",      "environmentId": "environment_01example0000000000000000x",      "serviceId": "svc_01example0000000000000000x"    }  },  "profile": {    "displayName": "My app",    "avatarUrl": "https://example.com",    "bannerUrl": "https://example.com",    "bio": "example",    "location": "example",    "links": {      "key": "example"    }  },  "counts": {    "followers": 0,    "following": 0,    "apps": 0  },  "createdAt": "2026-01-01T00:00:00.000Z"}

Update account profile

PATCH/accounts/{accountId}
Rolesreadereditoradminowner
Parameters
accountIdstringpathrequired
Request body
profileobject

Editable profile fields. Absent leaves a key alone; `null` clears it.

Returns
200 · AccountDto
idstringread-only

Account identifier.

handlestringrequired

Global-unique account handle — the URL/CLI slug.

tier"Free" | "Plus" | "Pro" | "Enterprise"read-only

The account’s plan, by display name. The internal Free sub-tiers all report `Free`.

status"active" | "suspended" | "pending_deletion" | "provisioning_pending" | "payment_pending"read-only

Account lifecycle status. `provisioning_pending`: the hosting environment is still being set up — deploys are paused until provisioning completes (retry via POST /accounts/:id/reprovision). `payment_pending`: a secondary account that has never been paid for — it has no hosting environment and every write is refused with 402 until a subscription starts.

deleteAtstring <date-time>read-only

Scheduled hard-delete time; set iff `status === 'pending_deletion'`.

isPrimarybooleanread-only

Whether this is the user's primary account (created during onboarding).

onboardingStateobjectread-only

The account's onboarding state machine position.

profileobjectrequired

Editable account profile (avatar, banner, bio, location, links).

countsobjectread-only

Denormalized social/app counts for this account.

createdAtstring <date-time>read-only

Account creation timestamp.

cURL
PATCH /accounts/{accountId}
bash
curl https://api.koo.io/accounts/:accountId \  -X PATCH \  -H "Authorization: Bearer kc_your_api_token" \  -H "Content-Type: application/json" \  -d '{}'
Response
json
{  "id": "acct_01example0000000000000000x",  "handle": "alice",  "tier": "Free",  "status": "active",  "deleteAt": "2026-01-01T00:00:00.000Z",  "isPrimary": true,  "onboardingState": {    "step": "SIGNED_IN",    "cliDeploy": {      "projectId": "project_01example0000000000000000x",      "environmentId": "environment_01example0000000000000000x",      "serviceId": "svc_01example0000000000000000x"    }  },  "profile": {    "displayName": "My app",    "avatarUrl": "https://example.com",    "bannerUrl": "https://example.com",    "bio": "example",    "location": "example",    "links": {      "key": "example"    }  },  "counts": {    "followers": 0,    "following": 0,    "apps": 0  },  "createdAt": "2026-01-01T00:00:00.000Z"}

Get the account's plan allowances

GET/accounts/{accountId}/entitlements
Rolesreadereditoradminowner

Plan allowances only — what the plan allows, resolved per service type (cpu/memory + replica ceilings), the deployment history depth, account limits, and the deploy locations available to the account. Stable and cacheable (changes only on a tier change); live counts are GET /accounts/:accountId/usage.

Parameters
accountIdstringpathrequired
Returns
200 · AccountEntitlementsDto
servicesRecord<string, object>read-only

Per-service-type allowances.

deploymentHistoryDepthintegerread-only

How many recent deployment versions per service can be rolled back to on your plan.

featuresobjectread-only

Plan capability flags (git auto-deploy, private apps, multi-region, priority support).

locationsstring[]read-only

Deploy locations available to this account (the set a service may run in).

limitsobjectread-only

Account-level plan ceilings, each with the plan that raises it.

cURL
GET /accounts/{accountId}/entitlements
bash
curl https://api.koo.io/accounts/:accountId/entitlements \  -H "Authorization: Bearer kc_your_api_token"
Response
json
{  "services": {    "key": {      "enabled": true,      "maxCpu": 0,      "maxMemory": 0,      "maxReplicas": 0,      "upgradeTo": "example"    }  },  "deploymentHistoryDepth": 0,  "features": {    "customDomains": true,    "autoDeployGit": true,    "multiRegion": true,    "prioritySupport": true,    "privateImages": true  },  "locations": [    "example"  ],  "limits": {    "maxProjects": {      "limit": 20,      "upgradeTo": "example"    },    "maxServicesByType": {      "web": {        "limit": 20,        "upgradeTo": "example"      },      "postgres": {        "limit": 20,        "upgradeTo": "example"      },      "redis": {        "limit": 20,        "upgradeTo": "example"      }    },    "maxEnvironments": {      "limit": 20,      "upgradeTo": "example"    },    "maxSeats": {      "limit": 20,      "upgradeTo": "example"    },    "maxLocationsPerService": {      "limit": 20,      "upgradeTo": "example"    },    "customDomains": {      "limit": 20,      "upgradeTo": "example"    }  }}

Get the account's live usage counts

GET/accounts/{accountId}/usage
Rolesreadereditoradminowner

Live counts (computed on read) for usage-vs-limit displays: projects, members, custom domains, and running services per type. `locations` is the number of deploy locations ENABLED on the account (the selectable set), not locations currently in use.

Parameters
accountIdstringpathrequired
Returns
200 · AccountUsageDto
projectsintegerread-only

Projects the account currently has.

environmentsintegerread-only

Active environments across the account (all projects) — gated by `maxEnvironments`.

servicesintegerread-only

Active services across the account (all projects/environments) — the account-wide total. Services are capped PER TYPE (`maxServicesByType`); see `servicesByType` for the per-type counts that cap gates on.

membersintegerread-only

Members on the account.

customDomainsintegerread-only

Custom domains across the account.

locationsintegerread-only

Number of deploy locations ENABLED on the account (the selectable set) — not locations currently in use.

servicesByTypeRecord<string, integer>read-only

Running service count per type.

cURL
GET /accounts/{accountId}/usage
bash
curl https://api.koo.io/accounts/:accountId/usage \  -H "Authorization: Bearer kc_your_api_token"
Response
json
{  "projects": 0,  "environments": 0,  "services": 0,  "members": 0,  "customDomains": 0,  "locations": 0,  "servicesByType": {    "key": 0  }}

Retry provisioning the account’s hosting environment

POST/accounts/{accountId}/reprovision
Rolesreadereditoradminowner

Re-runs the provisioning step for an account whose environment setup did not complete (status `provisioning_pending`). Idempotent — a fully provisioned account returns unchanged. Returns 412 when the remaining setup needs a manual step from the Koo team (retrying cannot complete it).

Parameters
accountIdstringpathrequired
Returns
200 · AccountDto412 · ErrorEnvelopeDto502 · ErrorEnvelopeDto
idstringread-only

Account identifier.

handlestringrequired

Global-unique account handle — the URL/CLI slug.

tier"Free" | "Plus" | "Pro" | "Enterprise"read-only

The account’s plan, by display name. The internal Free sub-tiers all report `Free`.

status"active" | "suspended" | "pending_deletion" | "provisioning_pending" | "payment_pending"read-only

Account lifecycle status. `provisioning_pending`: the hosting environment is still being set up — deploys are paused until provisioning completes (retry via POST /accounts/:id/reprovision). `payment_pending`: a secondary account that has never been paid for — it has no hosting environment and every write is refused with 402 until a subscription starts.

deleteAtstring <date-time>read-only

Scheduled hard-delete time; set iff `status === 'pending_deletion'`.

isPrimarybooleanread-only

Whether this is the user's primary account (created during onboarding).

onboardingStateobjectread-only

The account's onboarding state machine position.

profileobjectrequired

Editable account profile (avatar, banner, bio, location, links).

countsobjectread-only

Denormalized social/app counts for this account.

createdAtstring <date-time>read-only

Account creation timestamp.

cURL
POST /accounts/{accountId}/reprovision
bash
curl https://api.koo.io/accounts/:accountId/reprovision \  -X POST \  -H "Authorization: Bearer kc_your_api_token"
Response
json
{  "id": "acct_01example0000000000000000x",  "handle": "alice",  "tier": "Free",  "status": "active",  "deleteAt": "2026-01-01T00:00:00.000Z",  "isPrimary": true,  "onboardingState": {    "step": "SIGNED_IN",    "cliDeploy": {      "projectId": "project_01example0000000000000000x",      "environmentId": "environment_01example0000000000000000x",      "serviceId": "svc_01example0000000000000000x"    }  },  "profile": {    "displayName": "My app",    "avatarUrl": "https://example.com",    "bannerUrl": "https://example.com",    "bio": "example",    "location": "example",    "links": {      "key": "example"    }  },  "counts": {    "followers": 0,    "following": 0,    "apps": 0  },  "createdAt": "2026-01-01T00:00:00.000Z"}

Change the account plan (tier)

PATCH/accounts/{accountId}/subscription
Rolesreadereditoradminowner

Upgrade or downgrade the account tier. An over-limit downgrade returns 409 `downgrade_requires_resolution` with the dimensions over the target — resubmit with `resolution:"auto-shrink"` (non-destructive) and/or `resolutionIds` (apps/domains to remove, with `confirmDataLoss:true`). Stripe proration lands in a later billing slice.

Parameters
accountIdstringpathrequired
Request body
tierIdstringrequired

Target tier id (e.g. `free`, `plus`, `pro`).

cadence"monthly" | "annual"

Billing cadence. Omit to keep the subscription’s current one. Switching to `annual` charges a year of plan + seats up front at the tier’s 10× price (the commitment discount) and Stripe prorates the change. Compute is unaffected — it always bills monthly in arrears for what you ran.

resolution"auto-shrink"

Resolve an over-limit downgrade NON-DESTRUCTIVELY (shrink resources/replicas/locations).

resolutionIdsstring[]

Ids of apps/custom-domains to REMOVE to fit the target tier (destructive you-pick).

confirmDataLossboolean

Must be true when `resolutionIds` deletes apps/services that hold data.

Returns
200 · AccountDto402 · ErrorEnvelopeDto403 · ErrorEnvelopeDto409 · ErrorEnvelopeDto
idstringread-only

Account identifier.

handlestringrequired

Global-unique account handle — the URL/CLI slug.

tier"Free" | "Plus" | "Pro" | "Enterprise"read-only

The account’s plan, by display name. The internal Free sub-tiers all report `Free`.

status"active" | "suspended" | "pending_deletion" | "provisioning_pending" | "payment_pending"read-only

Account lifecycle status. `provisioning_pending`: the hosting environment is still being set up — deploys are paused until provisioning completes (retry via POST /accounts/:id/reprovision). `payment_pending`: a secondary account that has never been paid for — it has no hosting environment and every write is refused with 402 until a subscription starts.

deleteAtstring <date-time>read-only

Scheduled hard-delete time; set iff `status === 'pending_deletion'`.

isPrimarybooleanread-only

Whether this is the user's primary account (created during onboarding).

onboardingStateobjectread-only

The account's onboarding state machine position.

profileobjectrequired

Editable account profile (avatar, banner, bio, location, links).

countsobjectread-only

Denormalized social/app counts for this account.

createdAtstring <date-time>read-only

Account creation timestamp.

cURL
PATCH /accounts/{accountId}/subscription
bash
curl https://api.koo.io/accounts/:accountId/subscription \  -X PATCH \  -H "Authorization: Bearer kc_your_api_token" \  -H "Content-Type: application/json" \  -d '{      "tierId": "tier_01example0000000000000000x"    }'
Response
json
{  "id": "acct_01example0000000000000000x",  "handle": "alice",  "tier": "Free",  "status": "active",  "deleteAt": "2026-01-01T00:00:00.000Z",  "isPrimary": true,  "onboardingState": {    "step": "SIGNED_IN",    "cliDeploy": {      "projectId": "project_01example0000000000000000x",      "environmentId": "environment_01example0000000000000000x",      "serviceId": "svc_01example0000000000000000x"    }  },  "profile": {    "displayName": "My app",    "avatarUrl": "https://example.com",    "bannerUrl": "https://example.com",    "bio": "example",    "location": "example",    "links": {      "key": "example"    }  },  "counts": {    "followers": 0,    "following": 0,    "apps": 0  },  "createdAt": "2026-01-01T00:00:00.000Z"}

List account members, newest first

GET/accounts/{accountId}/members
Rolesreadereditoradminowner
Parameters
cursorstringquery

Opaque cursor (page.nextCursor).

limitnumberquery

Page size (1–100, default 20).

accountIdstringpathrequired
Returns
200 · MembersPageDto
dataobject[]read-only
pageobjectread-only
cURL
GET /accounts/{accountId}/members
bash
curl https://api.koo.io/accounts/:accountId/members \  -H "Authorization: Bearer kc_your_api_token"
Response
json
{  "data": [    {      "user": {        "id": "acct_01example0000000000000000x",        "email": "dev@example.com"      },      "role": "owner"    }  ],  "page": {    "nextCursor": "example",    "hasMore": true  }}

Change a member role / transfer ownership

PATCH/accounts/{accountId}/members/{userId}
Rolesreadereditoradminowner
Parameters
userIdstringpathrequired
accountIdstringpathrequired
Request body
role"owner" | "admin" | "editor" | "reader"required

New role; `owner` transfers the sole owner to the target member.

Returns
200 · MembershipDto
accountobjectread-only

The member account (lightweight projection).

role"owner" | "admin" | "editor" | "reader"required

The member's role on the account.

tier"Free" | "Plus" | "Pro" | "Enterprise"read-only

The member account's plan, by display name (same values as the account view).

status"active" | "suspended" | "pending_deletion" | "provisioning_pending" | "payment_pending"read-only

The member account's lifecycle status — lets the switcher render a `payment_pending` account as pending (finish setup) instead of as its placeholder Free tier.

cURL
PATCH /accounts/{accountId}/members/{userId}
bash
curl https://api.koo.io/accounts/:accountId/members/:userId \  -X PATCH \  -H "Authorization: Bearer kc_your_api_token" \  -H "Content-Type: application/json" \  -d '{      "role": "owner"    }'
Response
json
{  "account": {    "id": "acct_01example0000000000000000x",    "handle": "alice",    "displayName": "My app",    "avatarUrl": "https://example.com"  },  "role": "owner",  "tier": "Free",  "status": "active"}

Remove a member

DELETE/accounts/{accountId}/members/{userId}
Rolesreadereditoradminowner
Parameters
userIdstringpathrequired
accountIdstringpathrequired
Returns
204
cURL
DELETE /accounts/{accountId}/members/{userId}
bash
curl https://api.koo.io/accounts/:accountId/members/:userId \  -X DELETE \  -H "Authorization: Bearer kc_your_api_token"

List account API tokens

GET/accounts/{accountId}/tokens
Rolesreadereditoradminowner
Parameters
accountIdstringpathrequired
Returns
200 · TokenDto[]
cURL
GET /accounts/{accountId}/tokens
bash
curl https://api.koo.io/accounts/:accountId/tokens \  -H "Authorization: Bearer kc_your_api_token"
Response
json
[  {    "id": "acct_01example0000000000000000x",    "name": "my-app",    "role": "reader",    "prefix": "example",    "createdAt": "2026-01-01T00:00:00.000Z",    "lastUsedAt": "2026-01-01T00:00:00.000Z",    "expiresAt": "2026-01-01T00:00:00.000Z",    "revokedAt": "2026-01-01T00:00:00.000Z"  }]

Create an account API token

POST/accounts/{accountId}/tokens
Rolesreadereditoradminowner

Returns the plaintext `kc_…` secret ONCE — it is never retrievable again.

Parameters
accountIdstringpathrequired
Request body
namestringrequired

A label so you can recognize the token later (e.g. the CI that uses it).

role"reader" | "editor"required

Token role: `editor` (Write) can deploy and manage services; `reader` (Read) is view-only.

expiresInDaysinteger | null

Days until the token expires; null = never expires.

Returns
200 · CreatedTokenDto
tokenobjectread-only

The created token (without the secret).

secretstringread-only

The plaintext `kc_…` token. Shown ONCE — store it now; Koo cannot show it again.

cURL
POST /accounts/{accountId}/tokens
bash
curl https://api.koo.io/accounts/:accountId/tokens \  -X POST \  -H "Authorization: Bearer kc_your_api_token" \  -H "Content-Type: application/json" \  -d '{      "name": "my-app",      "role": "reader"    }'
Response
json
{  "token": {    "id": "acct_01example0000000000000000x",    "name": "my-app",    "role": "reader",    "prefix": "example",    "createdAt": "2026-01-01T00:00:00.000Z",    "lastUsedAt": "2026-01-01T00:00:00.000Z",    "expiresAt": "2026-01-01T00:00:00.000Z",    "revokedAt": "2026-01-01T00:00:00.000Z"  },  "secret": "example"}

Revoke an account API token

DELETE/accounts/{accountId}/tokens/{tokenId}
Rolesreadereditoradminowner
Parameters
tokenIdstringpathrequired
accountIdstringpathrequired
Returns
204
cURL
DELETE /accounts/{accountId}/tokens/{tokenId}
bash
curl https://api.koo.io/accounts/:accountId/tokens/:tokenId \  -X DELETE \  -H "Authorization: Bearer kc_your_api_token"

The account’s invitations, newest first

GET/accounts/{accountId}/invitations
Rolesreadereditoradminowner

Owner-only. Never returns a token — a link is readable exactly once, at creation.

Parameters
accountIdstringpathrequired
Returns
200 · InvitationDto[]
cURL
GET /accounts/{accountId}/invitations
bash
curl https://api.koo.io/accounts/:accountId/invitations \  -H "Authorization: Bearer kc_your_api_token"
Response
json
[  {    "id": "acct_01example0000000000000000x",    "accountId": "acct_01example0000000000000000x",    "role": "owner",    "email": "dev@example.com",    "status": "pending",    "expiresAt": "2026-01-01T00:00:00.000Z",    "invitedByUserId": "invitedbyuser_01example0000000000000000x",    "acceptedByUserId": "acceptedbyuser_01example0000000000000000x",    "acceptedAt": "2026-01-01T00:00:00.000Z",    "createdAt": "2026-01-01T00:00:00.000Z"  }]

Create a one-time invite link

POST/accounts/{accountId}/invitations
Rolesreadereditoradminowner

Owner-only. Returns the accept link **once** — only its hash is stored, so it can never be read again. `owner` is not an invitable role: an account has exactly one owner, and ownership is transferred through `PATCH /accounts/{accountId}/members/{userId}`. The link expires in 7 days.

Parameters
accountIdstringpathrequired
Request body
role"admin" | "editor" | "reader"required

The role the invited member joins at. `owner` is not invitable: an account has exactly one owner, and ownership is transferred deliberately via `PATCH /accounts/{accountId}/members/{userId}`.

emailstring <email>required
Returns
201 · InvitationLinkDto402 · ErrorEnvelopeDto403 · ErrorEnvelopeDto422 · ErrorEnvelopeDto
invitationobjectread-only
linkstring <uri>read-only
cURL
POST /accounts/{accountId}/invitations
bash
curl https://api.koo.io/accounts/:accountId/invitations \  -X POST \  -H "Authorization: Bearer kc_your_api_token" \  -H "Content-Type: application/json" \  -d '{      "role": "admin",      "email": "dev@example.com"    }'
Response
json
{  "invitation": {    "id": "acct_01example0000000000000000x",    "accountId": "acct_01example0000000000000000x",    "role": "owner",    "email": "dev@example.com",    "status": "pending",    "expiresAt": "2026-01-01T00:00:00.000Z",    "invitedByUserId": "invitedbyuser_01example0000000000000000x",    "acceptedByUserId": "acceptedbyuser_01example0000000000000000x",    "acceptedAt": "2026-01-01T00:00:00.000Z",    "createdAt": "2026-01-01T00:00:00.000Z"  },  "link": "https://example.com"}

Revoke a pending invite link

DELETE/accounts/{accountId}/invitations/{invitationId}
Rolesreadereditoradminowner

Idempotent for an already-revoked invite; refuses one that was already accepted (409).

Parameters
invitationIdstringpathrequired
accountIdstringpathrequired
Returns
204409 · ErrorEnvelopeDto
cURL
DELETE /accounts/{accountId}/invitations/{invitationId}
bash
curl https://api.koo.io/accounts/:accountId/invitations/:invitationId \  -X DELETE \  -H "Authorization: Bearer kc_your_api_token"

Accept an invite link and join the account

POST/invitations/{token}/accept

Signed-in humans only, captcha-gated (`X-Turnstile-Token`). Consumes the link and creates the membership at the invited role. If the account is at its seat limit the invite is **not** consumed and the response is `402 quota_exceeded` naming the plan to upgrade to — the owner upgrades and the same link still works.

Parameters
tokenstringpathrequired

The one-time token from the invite link.

Returns
200 · AcceptInvitationResultDto402 · ErrorEnvelopeDto404 · ErrorEnvelopeDto409 · ErrorEnvelopeDto
accountIdstringread-only
handlestringread-only
role"owner" | "admin" | "editor" | "reader"read-only

Per-account RBAC role, decreasing privilege: `owner` (creator/top; transferable singleton — members, plan, billing + everything below) ⊃ `admin` (full operational control — deploy, environment/project create+delete, domains, secrets, account settings — minus members/plan/billing) ⊃ `editor` (edit/deploy) ⊃ `reader` (view).

cURL
POST /invitations/{token}/accept
bash
curl https://api.koo.io/invitations/:token/accept \  -X POST \  -H "Authorization: Bearer kc_your_api_token"
Response
json
{  "accountId": "acct_01example0000000000000000x",  "handle": "alice",  "role": "owner"}