Accounts
The account audit log, newest first
Opaque cursor (page.nextCursor).
Page size (1–100, default 20).
curl https://api.koo.io/accounts/:accountId/events \ -H "Authorization: Bearer kc_your_api_token"{ "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)
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.
Global-unique handle for the new account — the URL/CLI slug.
Optional pretty name, stored on `profile.displayName`. UIs fall back to the handle.
Account identifier.
Global-unique account handle — the URL/CLI slug.
The account’s plan, by display name. The internal Free sub-tiers all report `Free`.
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.
Scheduled hard-delete time; set iff `status === 'pending_deletion'`.
Whether this is the user's primary account (created during onboarding).
The account's onboarding state machine position.
Editable account profile (avatar, banner, bio, location, links).
Denormalized social/app counts for this account.
Account creation timestamp.
curl https://api.koo.io/accounts \ -X POST \ -H "Authorization: Bearer kc_your_api_token" \ -H "Content-Type: application/json" \ -d '{ "handle": "alice" }'{ "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
Account identifier.
Global-unique account handle — the URL/CLI slug.
The account’s plan, by display name. The internal Free sub-tiers all report `Free`.
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.
Scheduled hard-delete time; set iff `status === 'pending_deletion'`.
Whether this is the user's primary account (created during onboarding).
The account's onboarding state machine position.
Editable account profile (avatar, banner, bio, location, links).
Denormalized social/app counts for this account.
Account creation timestamp.
curl https://api.koo.io/accounts/:accountId \ -H "Authorization: Bearer kc_your_api_token"{ "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
Editable profile fields. Absent leaves a key alone; `null` clears it.
Account identifier.
Global-unique account handle — the URL/CLI slug.
The account’s plan, by display name. The internal Free sub-tiers all report `Free`.
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.
Scheduled hard-delete time; set iff `status === 'pending_deletion'`.
Whether this is the user's primary account (created during onboarding).
The account's onboarding state machine position.
Editable account profile (avatar, banner, bio, location, links).
Denormalized social/app counts for this account.
Account creation timestamp.
curl https://api.koo.io/accounts/:accountId \ -X PATCH \ -H "Authorization: Bearer kc_your_api_token" \ -H "Content-Type: application/json" \ -d '{}'{ "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
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.
Per-service-type allowances.
How many recent deployment versions per service can be rolled back to on your plan.
Plan capability flags (git auto-deploy, private apps, multi-region, priority support).
Deploy locations available to this account (the set a service may run in).
Account-level plan ceilings, each with the plan that raises it.
curl https://api.koo.io/accounts/:accountId/entitlements \ -H "Authorization: Bearer kc_your_api_token"{ "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
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.
Projects the account currently has.
Active environments across the account (all projects) — gated by `maxEnvironments`.
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.
Members on the account.
Custom domains across the account.
Number of deploy locations ENABLED on the account (the selectable set) — not locations currently in use.
Running service count per type.
curl https://api.koo.io/accounts/:accountId/usage \ -H "Authorization: Bearer kc_your_api_token"{ "projects": 0, "environments": 0, "services": 0, "members": 0, "customDomains": 0, "locations": 0, "servicesByType": { "key": 0 }}Retry provisioning the account’s hosting environment
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).
Account identifier.
Global-unique account handle — the URL/CLI slug.
The account’s plan, by display name. The internal Free sub-tiers all report `Free`.
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.
Scheduled hard-delete time; set iff `status === 'pending_deletion'`.
Whether this is the user's primary account (created during onboarding).
The account's onboarding state machine position.
Editable account profile (avatar, banner, bio, location, links).
Denormalized social/app counts for this account.
Account creation timestamp.
curl https://api.koo.io/accounts/:accountId/reprovision \ -X POST \ -H "Authorization: Bearer kc_your_api_token"{ "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)
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.
Target tier id (e.g. `free`, `plus`, `pro`).
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.
Resolve an over-limit downgrade NON-DESTRUCTIVELY (shrink resources/replicas/locations).
Ids of apps/custom-domains to REMOVE to fit the target tier (destructive you-pick).
Must be true when `resolutionIds` deletes apps/services that hold data.
Account identifier.
Global-unique account handle — the URL/CLI slug.
The account’s plan, by display name. The internal Free sub-tiers all report `Free`.
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.
Scheduled hard-delete time; set iff `status === 'pending_deletion'`.
Whether this is the user's primary account (created during onboarding).
The account's onboarding state machine position.
Editable account profile (avatar, banner, bio, location, links).
Denormalized social/app counts for this account.
Account creation timestamp.
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" }'{ "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
Opaque cursor (page.nextCursor).
Page size (1–100, default 20).
curl https://api.koo.io/accounts/:accountId/members \ -H "Authorization: Bearer kc_your_api_token"{ "data": [ { "user": { "id": "acct_01example0000000000000000x", "email": "dev@example.com" }, "role": "owner" } ], "page": { "nextCursor": "example", "hasMore": true }}Change a member role / transfer ownership
New role; `owner` transfers the sole owner to the target member.
The member account (lightweight projection).
The member's role on the account.
The member account's plan, by display name (same values as the account view).
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 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" }'{ "account": { "id": "acct_01example0000000000000000x", "handle": "alice", "displayName": "My app", "avatarUrl": "https://example.com" }, "role": "owner", "tier": "Free", "status": "active"}Remove a member
curl https://api.koo.io/accounts/:accountId/members/:userId \ -X DELETE \ -H "Authorization: Bearer kc_your_api_token"List account API tokens
curl https://api.koo.io/accounts/:accountId/tokens \ -H "Authorization: Bearer kc_your_api_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" }]Create an account API token
Returns the plaintext `kc_…` secret ONCE — it is never retrievable again.
A label so you can recognize the token later (e.g. the CI that uses it).
Token role: `editor` (Write) can deploy and manage services; `reader` (Read) is view-only.
Days until the token expires; null = never expires.
The created token (without the secret).
The plaintext `kc_…` token. Shown ONCE — store it now; Koo cannot show it again.
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" }'{ "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
curl https://api.koo.io/accounts/:accountId/tokens/:tokenId \ -X DELETE \ -H "Authorization: Bearer kc_your_api_token"The account’s invitations, newest first
Owner-only. Never returns a token — a link is readable exactly once, at creation.
curl https://api.koo.io/accounts/:accountId/invitations \ -H "Authorization: Bearer kc_your_api_token"[ { "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
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.
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}`.
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" }'{ "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
Idempotent for an already-revoked invite; refuses one that was already accepted (409).
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
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.
The one-time token from the invite link.
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 https://api.koo.io/invitations/:token/accept \ -X POST \ -H "Authorization: Bearer kc_your_api_token"{ "accountId": "acct_01example0000000000000000x", "handle": "alice", "role": "owner"}