# Accounts

### The account audit log, newest first

`GET /accounts/{accountId}/events`

**Required role:** reader

**Parameters**

- `cursor` _(query)_ · `string` · optional — Opaque cursor (page.nextCursor).
- `limit` _(query)_ · `number` · optional — Page size (1–100, default 20).
- `accountId` _(path)_ · `string` · required

**Returns**

- `200` · `AccountEventsPageDto`

Attributes of `AccountEventsPageDto`:

- `data` · `object[]` · read-only
- `page` · `object` · read-only

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/events \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```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**

- `handle` · `string` · required — Global-unique handle for the new account — the URL/CLI slug.
- `displayName` · `string` · optional — Optional pretty name, stored on `profile.displayName`. UIs fall back to the handle.

**Returns**

- `201` · `AccountDto`
- `400` · `ErrorEnvelopeDto` — captcha_required
- `403` · `ErrorEnvelopeDto` — service accounts may not create accounts
- `409` · `ErrorEnvelopeDto` — handle taken/reserved/blocked · an unpaid account already exists · onboarding unfinished

Attributes of `AccountDto`:

- `id` · `string` · read-only — Account identifier.
- `handle` · `string` · required — 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.
- `deleteAt` · `string <date-time>` · read-only — Scheduled hard-delete time; set iff `status === 'pending_deletion'`.
- `isPrimary` · `boolean` · read-only — Whether this is the user's primary account (created during onboarding).
- `onboardingState` · `object` · read-only — The account's onboarding state machine position.
- `profile` · `object` · required — Editable account profile (avatar, banner, bio, location, links).
- `counts` · `object` · read-only — Denormalized social/app counts for this account.
- `createdAt` · `string <date-time>` · read-only — Account creation timestamp.

**Example request**

```bash
curl https://api.koo.io/accounts \
  -X POST \
  -H "Authorization: Bearer kc_your_api_token" \
  -H "Content-Type: application/json" \
  -d '{
      "handle": "alice"
    }'
```

**Example response** (`201`)

```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}`

**Required role:** reader

**Parameters**

- `accountId` _(path)_ · `string` · required

**Returns**

- `200` · `AccountDto`

Attributes of `AccountDto`:

- `id` · `string` · read-only — Account identifier.
- `handle` · `string` · required — 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.
- `deleteAt` · `string <date-time>` · read-only — Scheduled hard-delete time; set iff `status === 'pending_deletion'`.
- `isPrimary` · `boolean` · read-only — Whether this is the user's primary account (created during onboarding).
- `onboardingState` · `object` · read-only — The account's onboarding state machine position.
- `profile` · `object` · required — Editable account profile (avatar, banner, bio, location, links).
- `counts` · `object` · read-only — Denormalized social/app counts for this account.
- `createdAt` · `string <date-time>` · read-only — Account creation timestamp.

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```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}`

**Required role:** admin

**Parameters**

- `accountId` _(path)_ · `string` · required

**Request body**

- `profile` · `object` · optional — Editable profile fields. Absent leaves a key alone; `null` clears it.

**Returns**

- `200` · `AccountDto`

Attributes of `AccountDto`:

- `id` · `string` · read-only — Account identifier.
- `handle` · `string` · required — 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.
- `deleteAt` · `string <date-time>` · read-only — Scheduled hard-delete time; set iff `status === 'pending_deletion'`.
- `isPrimary` · `boolean` · read-only — Whether this is the user's primary account (created during onboarding).
- `onboardingState` · `object` · read-only — The account's onboarding state machine position.
- `profile` · `object` · required — Editable account profile (avatar, banner, bio, location, links).
- `counts` · `object` · read-only — Denormalized social/app counts for this account.
- `createdAt` · `string <date-time>` · read-only — Account creation timestamp.

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId \
  -X PATCH \
  -H "Authorization: Bearer kc_your_api_token" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**Example response** (`200`)

```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`

**Required role:** reader

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**

- `accountId` _(path)_ · `string` · required

**Returns**

- `200` · `AccountEntitlementsDto`

Attributes of `AccountEntitlementsDto`:

- `services` · `Record<string, object>` · read-only — Per-service-type allowances.
- `deploymentHistoryDepth` · `integer` · read-only — How many recent deployment versions per service can be rolled back to on your plan.
- `features` · `object` · read-only — Plan capability flags (git auto-deploy, private apps, multi-region, priority support).
- `locations` · `string[]` · read-only — Deploy locations available to this account (the set a service may run in).
- `limits` · `object` · read-only — Account-level plan ceilings, each with the plan that raises it.

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/entitlements \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```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`

**Required role:** reader

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**

- `accountId` _(path)_ · `string` · required

**Returns**

- `200` · `AccountUsageDto`

Attributes of `AccountUsageDto`:

- `projects` · `integer` · read-only — Projects the account currently has.
- `environments` · `integer` · read-only — Active environments across the account (all projects) — gated by `maxEnvironments`.
- `services` · `integer` · read-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.
- `members` · `integer` · read-only — Members on the account.
- `customDomains` · `integer` · read-only — Custom domains across the account.
- `locations` · `integer` · read-only — Number of deploy locations ENABLED on the account (the selectable set) — not locations currently in use.
- `servicesByType` · `Record<string, integer>` · read-only — Running service count per type.

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/usage \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```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`

**Required role:** admin

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**

- `accountId` _(path)_ · `string` · required

**Returns**

- `200` · `AccountDto`
- `412` · `ErrorEnvelopeDto` — account not eligible (suspended/deleting), or setup needs a manual step from the Koo team
- `502` · `ErrorEnvelopeDto` — provisioning still incomplete — retry later

Attributes of `AccountDto`:

- `id` · `string` · read-only — Account identifier.
- `handle` · `string` · required — 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.
- `deleteAt` · `string <date-time>` · read-only — Scheduled hard-delete time; set iff `status === 'pending_deletion'`.
- `isPrimary` · `boolean` · read-only — Whether this is the user's primary account (created during onboarding).
- `onboardingState` · `object` · read-only — The account's onboarding state machine position.
- `profile` · `object` · required — Editable account profile (avatar, banner, bio, location, links).
- `counts` · `object` · read-only — Denormalized social/app counts for this account.
- `createdAt` · `string <date-time>` · read-only — Account creation timestamp.

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/reprovision \
  -X POST \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```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`

**Required role:** owner

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**

- `accountId` _(path)_ · `string` · required

**Request body**

- `tierId` · `string` · required — Target tier id (e.g. `free`, `plus`, `pro`).
- `cadence` · `"monthly" | "annual"` · optional — 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"` · optional — Resolve an over-limit downgrade NON-DESTRUCTIVELY (shrink resources/replicas/locations).
- `resolutionIds` · `string[]` · optional — Ids of apps/custom-domains to REMOVE to fit the target tier (destructive you-pick).
- `confirmDataLoss` · `boolean` · optional — Must be true when `resolutionIds` deletes apps/services that hold data.

**Returns**

- `200` · `AccountDto`
- `402` · `ErrorEnvelopeDto` — quota_exceeded
- `403` · `ErrorEnvelopeDto` — forbidden — graduated/graduating, or not an account admin
- `409` · `ErrorEnvelopeDto` — downgrade_requires_resolution

Attributes of `AccountDto`:

- `id` · `string` · read-only — Account identifier.
- `handle` · `string` · required — 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.
- `deleteAt` · `string <date-time>` · read-only — Scheduled hard-delete time; set iff `status === 'pending_deletion'`.
- `isPrimary` · `boolean` · read-only — Whether this is the user's primary account (created during onboarding).
- `onboardingState` · `object` · read-only — The account's onboarding state machine position.
- `profile` · `object` · required — Editable account profile (avatar, banner, bio, location, links).
- `counts` · `object` · read-only — Denormalized social/app counts for this account.
- `createdAt` · `string <date-time>` · read-only — Account creation timestamp.

**Example request**

```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"
    }'
```

**Example response** (`200`)

```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`

**Required role:** reader

**Parameters**

- `cursor` _(query)_ · `string` · optional — Opaque cursor (page.nextCursor).
- `limit` _(query)_ · `number` · optional — Page size (1–100, default 20).
- `accountId` _(path)_ · `string` · required

**Returns**

- `200` · `MembersPageDto`

Attributes of `MembersPageDto`:

- `data` · `object[]` · read-only
- `page` · `object` · read-only

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/members \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```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}`

**Required role:** owner

**Parameters**

- `userId` _(path)_ · `string` · required
- `accountId` _(path)_ · `string` · required

**Request body**

- `role` · `"owner" | "admin" | "editor" | "reader"` · required — New role; `owner` transfers the sole owner to the target member.

**Returns**

- `200` · `MembershipDto`

Attributes of `MembershipDto`:

- `account` · `object` · read-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.

**Example request**

```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"
    }'
```

**Example response** (`200`)

```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}`

**Required role:** owner

**Parameters**

- `userId` _(path)_ · `string` · required
- `accountId` _(path)_ · `string` · required

**Returns**

- `204`

**Example request**

```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`

**Required role:** admin

**Parameters**

- `accountId` _(path)_ · `string` · required

**Returns**

- `200` · `TokenDto[]`

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/tokens \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```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`

**Required role:** admin

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

**Parameters**

- `accountId` _(path)_ · `string` · required

**Request body**

- `name` · `string` · required — 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.
- `expiresInDays` · `integer | null` · optional — Days until the token expires; null = never expires.

**Returns**

- `200` · `CreatedTokenDto`

Attributes of `CreatedTokenDto`:

- `token` · `object` · read-only — The created token (without the secret).
- `secret` · `string` · read-only — The plaintext `kc_…` token. Shown ONCE — store it now; Koo cannot show it again.

**Example request**

```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"
    }'
```

**Example response** (`200`)

```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}`

**Required role:** admin

**Parameters**

- `tokenId` _(path)_ · `string` · required
- `accountId` _(path)_ · `string` · required

**Returns**

- `204`

**Example request**

```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`

**Required role:** owner

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

**Parameters**

- `accountId` _(path)_ · `string` · required

**Returns**

- `200` · `InvitationDto[]`

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/invitations \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```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`

**Required role:** owner

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**

- `accountId` _(path)_ · `string` · required

**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}`.
- `email` · `string <email>` · required

**Returns**

- `201` · `InvitationLinkDto`
- `402` · `ErrorEnvelopeDto` — the account is at its seat limit
- `403` · `ErrorEnvelopeDto` — not the owner
- `422` · `ErrorEnvelopeDto` — role is not invitable

Attributes of `InvitationLinkDto`:

- `invitation` · `object` · read-only
- `link` · `string <uri>` · read-only

**Example request**

```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"
    }'
```

**Example response** (`201`)

```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}`

**Required role:** owner

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

**Parameters**

- `invitationId` _(path)_ · `string` · required
- `accountId` _(path)_ · `string` · required

**Returns**

- `204` — revoked
- `409` · `ErrorEnvelopeDto` — already accepted

**Example request**

```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**

- `token` _(path)_ · `string` · required — The one-time token from the invite link.

**Returns**

- `200` · `AcceptInvitationResultDto`
- `402` · `ErrorEnvelopeDto` — the account is at its seat limit
- `404` · `ErrorEnvelopeDto` — no such invitation
- `409` · `ErrorEnvelopeDto` — expired · revoked · already used · already a member

Attributes of `AcceptInvitationResultDto`:

- `accountId` · `string` · read-only
- `handle` · `string` · read-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).

**Example request**

```bash
curl https://api.koo.io/invitations/:token/accept \
  -X POST \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```json
{
  "accountId": "acct_01example0000000000000000x",
  "handle": "alice",
  "role": "owner"
}
```
