# Apps

### List the account's published apps (with live-derived URL + blockers), newest first

`GET /accounts/{accountId}/apps`

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

Attributes of `PublishedAppsPageDto`:

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

**Example request**

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

**Example response** (`200`)

```json
{
  "data": [
    {
      "id": "acct_01example0000000000000000x",
      "accountId": "acct_01example0000000000000000x",
      "projectId": "project_01example0000000000000000x",
      "environmentName": "example",
      "serviceName": "example",
      "urlChoice": "example",
      "slug": "my-app",
      "profile": {
        "displayName": "My app",
        "tagline": "example",
        "description": "example",
        "images": [
          "https://example.com"
        ],
        "youtubeId": "youtube_01example0000000000000000x",
        "videoCover": true,
        "category": "example",
        "tags": [
          "example"
        ],
        "links": {
          "website": "https://example.com",
          "repo": "https://example.com",
          "appStore": "https://example.com",
          "playStore": "https://example.com",
          "steam": "https://example.com"
        }
      },
      "url": "https://example.com",
      "blockers": [
        "service_missing"
      ],
      "createdAt": "2026-01-01T00:00:00.000Z",
      "updatedAt": "2026-01-01T00:00:00.000Z"
    }
  ],
  "page": {
    "nextCursor": "example",
    "hasMore": true
  }
}
```

### Publish a service as a showcase app

`POST /accounts/{accountId}/apps`

**Required role:** admin

Validates the pointer live (project in account; environment + exposed web service by name; URL is the generated koo URL or an active custom domain of that service) and the publish gate (tagline + category).

**Parameters**

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

**Request body**

- `projectId` · `string` · required — The project the service to publish lives in (must belong to the account).
- `environmentName` · `string` · required — Name of the environment the service lives in.
- `serviceName` · `string` · required — Name of the exposed web service to publish (by name).
- `urlChoice` · `string` · optional — Either 'generated' (the koo URL from the service's status) or one of the service's active custom domain hostnames.
- `slug` · `string` · required — The public URL slug under the owner handle (url-safe word, 3-16 chars, account-unique).
- `profile` · `object` · required — The initial listing content (tagline + category are required to publish).

**Returns**

- `200` · `PublishedAppDto`
- `409` · `ErrorEnvelopeDto` — slug or service already published

Attributes of `PublishedAppDto`:

- `id` · `string` · read-only — Identifier of the published app.
- `accountId` · `string` · read-only — Identifier of the owning account.
- `projectId` · `string` · required — Identifier of the project the pointed-at service lives in.
- `environmentName` · `string` · required — Name of the environment the pointed-at service lives in (by name, not id).
- `serviceName` · `string` · required — Name of the exposed web service this listing points at (by name, not id).
- `urlChoice` · `string` · required — Either 'generated' (the koo URL from the service's status) or one of the service's active custom domain hostnames.
- `slug` · `string` · required — URL slug of the app under the owner's handle (/@handle/:slug).
- `profile` · `object` · required
- `url` · `string <uri>` · read-only — The live-derived public URL to visit, when resolvable (absent while degraded with no fallback).
- `blockers` · `"service_missing" | "not_exposed" | "domain_missing"[]` · read-only — Live-derived degradations on the pointer — the honest reasons shown to the owner. Empty when healthy.
- `createdAt` · `string <date-time>` · read-only — When the app was published.
- `updatedAt` · `string <date-time>` · read-only — When the app or its listing was last updated.

**Example request**

```bash
curl https://api.koo.io/accounts/:accountId/apps \
  -X POST \
  -H "Authorization: Bearer kc_your_api_token" \
  -H "Content-Type: application/json" \
  -d '{
      "projectId": "project_01example0000000000000000x",
      "environmentName": "example",
      "serviceName": "example",
      "slug": "my-app",
      "profile": {}
    }'
```

**Example response** (`200`)

```json
{
  "id": "acct_01example0000000000000000x",
  "accountId": "acct_01example0000000000000000x",
  "projectId": "project_01example0000000000000000x",
  "environmentName": "example",
  "serviceName": "example",
  "urlChoice": "example",
  "slug": "my-app",
  "profile": {
    "displayName": "My app",
    "tagline": "example",
    "description": "example",
    "images": [
      "https://example.com"
    ],
    "youtubeId": "youtube_01example0000000000000000x",
    "videoCover": true,
    "category": "example",
    "tags": [
      "example"
    ],
    "links": {
      "website": "https://example.com",
      "repo": "https://example.com",
      "appStore": "https://example.com",
      "playStore": "https://example.com",
      "steam": "https://example.com"
    }
  },
  "url": "https://example.com",
  "blockers": [
    "service_missing"
  ],
  "createdAt": "2026-01-01T00:00:00.000Z",
  "updatedAt": "2026-01-01T00:00:00.000Z"
}
```

### Get a published app (owner view)

`GET /accounts/{accountId}/apps/{appId}`

**Required role:** reader

**Parameters**

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

**Returns**

- `200` · `PublishedAppDto`
- `404` · `ErrorEnvelopeDto` — not_found

Attributes of `PublishedAppDto`:

- `id` · `string` · read-only — Identifier of the published app.
- `accountId` · `string` · read-only — Identifier of the owning account.
- `projectId` · `string` · required — Identifier of the project the pointed-at service lives in.
- `environmentName` · `string` · required — Name of the environment the pointed-at service lives in (by name, not id).
- `serviceName` · `string` · required — Name of the exposed web service this listing points at (by name, not id).
- `urlChoice` · `string` · required — Either 'generated' (the koo URL from the service's status) or one of the service's active custom domain hostnames.
- `slug` · `string` · required — URL slug of the app under the owner's handle (/@handle/:slug).
- `profile` · `object` · required
- `url` · `string <uri>` · read-only — The live-derived public URL to visit, when resolvable (absent while degraded with no fallback).
- `blockers` · `"service_missing" | "not_exposed" | "domain_missing"[]` · read-only — Live-derived degradations on the pointer — the honest reasons shown to the owner. Empty when healthy.
- `createdAt` · `string <date-time>` · read-only — When the app was published.
- `updatedAt` · `string <date-time>` · read-only — When the app or its listing was last updated.

**Example request**

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

**Example response** (`200`)

```json
{
  "id": "acct_01example0000000000000000x",
  "accountId": "acct_01example0000000000000000x",
  "projectId": "project_01example0000000000000000x",
  "environmentName": "example",
  "serviceName": "example",
  "urlChoice": "example",
  "slug": "my-app",
  "profile": {
    "displayName": "My app",
    "tagline": "example",
    "description": "example",
    "images": [
      "https://example.com"
    ],
    "youtubeId": "youtube_01example0000000000000000x",
    "videoCover": true,
    "category": "example",
    "tags": [
      "example"
    ],
    "links": {
      "website": "https://example.com",
      "repo": "https://example.com",
      "appStore": "https://example.com",
      "playStore": "https://example.com",
      "steam": "https://example.com"
    }
  },
  "url": "https://example.com",
  "blockers": [
    "service_missing"
  ],
  "createdAt": "2026-01-01T00:00:00.000Z",
  "updatedAt": "2026-01-01T00:00:00.000Z"
}
```

### Re-point a published app at a different service and/or URL

`PATCH /accounts/{accountId}/apps/{appId}`

**Required role:** admin

Re-validates the resulting pointer live (same rules as publish). A structural change to the public listing, so it requires the publish permission.

**Parameters**

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

**Request body**

- `environmentName` · `string` · optional — New environment name for the pointer.
- `serviceName` · `string` · optional — New exposed web service name for the pointer.
- `urlChoice` · `string` · optional — New URL choice ('generated' or an active custom domain hostname).

**Returns**

- `200` · `PublishedAppDto`

Attributes of `PublishedAppDto`:

- `id` · `string` · read-only — Identifier of the published app.
- `accountId` · `string` · read-only — Identifier of the owning account.
- `projectId` · `string` · required — Identifier of the project the pointed-at service lives in.
- `environmentName` · `string` · required — Name of the environment the pointed-at service lives in (by name, not id).
- `serviceName` · `string` · required — Name of the exposed web service this listing points at (by name, not id).
- `urlChoice` · `string` · required — Either 'generated' (the koo URL from the service's status) or one of the service's active custom domain hostnames.
- `slug` · `string` · required — URL slug of the app under the owner's handle (/@handle/:slug).
- `profile` · `object` · required
- `url` · `string <uri>` · read-only — The live-derived public URL to visit, when resolvable (absent while degraded with no fallback).
- `blockers` · `"service_missing" | "not_exposed" | "domain_missing"[]` · read-only — Live-derived degradations on the pointer — the honest reasons shown to the owner. Empty when healthy.
- `createdAt` · `string <date-time>` · read-only — When the app was published.
- `updatedAt` · `string <date-time>` · read-only — When the app or its listing was last updated.

**Example request**

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

**Example response** (`200`)

```json
{
  "id": "acct_01example0000000000000000x",
  "accountId": "acct_01example0000000000000000x",
  "projectId": "project_01example0000000000000000x",
  "environmentName": "example",
  "serviceName": "example",
  "urlChoice": "example",
  "slug": "my-app",
  "profile": {
    "displayName": "My app",
    "tagline": "example",
    "description": "example",
    "images": [
      "https://example.com"
    ],
    "youtubeId": "youtube_01example0000000000000000x",
    "videoCover": true,
    "category": "example",
    "tags": [
      "example"
    ],
    "links": {
      "website": "https://example.com",
      "repo": "https://example.com",
      "appStore": "https://example.com",
      "playStore": "https://example.com",
      "steam": "https://example.com"
    }
  },
  "url": "https://example.com",
  "blockers": [
    "service_missing"
  ],
  "createdAt": "2026-01-01T00:00:00.000Z",
  "updatedAt": "2026-01-01T00:00:00.000Z"
}
```

### Unpublish an app (soft-delete; republish re-creates)

`DELETE /accounts/{accountId}/apps/{appId}`

**Required role:** admin

**Parameters**

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

**Returns**

- `204`

**Example request**

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

### Edit a published app listing (content, tags, links, image order)

`PATCH /accounts/{accountId}/apps/{appId}/profile`

**Required role:** editor

Owner/editor edit of the showcase fields (displayName/tagline/description/youtubeId/category/tags/links + image reorder/remove). Adding an image goes through the upload commit (scope appImage). Repopulates discovery search.

**Parameters**

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

**Request body**

- `displayName` · `string | null` · optional — Public pretty name (null to clear — the app slug is then shown).
- `tagline` · `string | null` · optional — Short one-line summary (null to clear).
- `description` · `string | null` · optional — Long-form description for the showcase page (null to clear).
- `youtubeId` · `string | null` · optional — YouTube video id embedded as a demo/trailer (null to clear).
- `videoCover` · `boolean` · optional — When a video is set, use the video's YouTube thumbnail as the cover (true) or fall back to the first gallery image (false).
- `category` · `"web" | "api" | "devtools" | "ai-ml" | "data" | "bots" | "games" | "ecommerce" | "social" | "productivity" | "education" | "other" | null` · optional — Discovery category slug; must be one of the configured categories (null to clear).
- `tags` · `string[]` · optional — Freeform discovery tags (empty array to clear).
- `links` · `object` · optional — External links shown on the showcase page (replaces the whole set).
- `images` · `string <uri>[]` · optional — The reordered/pruned gallery (replaces the order; images[0] is the cover). Only URLs already committed to this app may appear — adding a new image goes through the upload commit.

**Returns**

- `200` · `PublishedAppDto`

Attributes of `PublishedAppDto`:

- `id` · `string` · read-only — Identifier of the published app.
- `accountId` · `string` · read-only — Identifier of the owning account.
- `projectId` · `string` · required — Identifier of the project the pointed-at service lives in.
- `environmentName` · `string` · required — Name of the environment the pointed-at service lives in (by name, not id).
- `serviceName` · `string` · required — Name of the exposed web service this listing points at (by name, not id).
- `urlChoice` · `string` · required — Either 'generated' (the koo URL from the service's status) or one of the service's active custom domain hostnames.
- `slug` · `string` · required — URL slug of the app under the owner's handle (/@handle/:slug).
- `profile` · `object` · required
- `url` · `string <uri>` · read-only — The live-derived public URL to visit, when resolvable (absent while degraded with no fallback).
- `blockers` · `"service_missing" | "not_exposed" | "domain_missing"[]` · read-only — Live-derived degradations on the pointer — the honest reasons shown to the owner. Empty when healthy.
- `createdAt` · `string <date-time>` · read-only — When the app was published.
- `updatedAt` · `string <date-time>` · read-only — When the app or its listing was last updated.

**Example request**

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

**Example response** (`200`)

```json
{
  "id": "acct_01example0000000000000000x",
  "accountId": "acct_01example0000000000000000x",
  "projectId": "project_01example0000000000000000x",
  "environmentName": "example",
  "serviceName": "example",
  "urlChoice": "example",
  "slug": "my-app",
  "profile": {
    "displayName": "My app",
    "tagline": "example",
    "description": "example",
    "images": [
      "https://example.com"
    ],
    "youtubeId": "youtube_01example0000000000000000x",
    "videoCover": true,
    "category": "example",
    "tags": [
      "example"
    ],
    "links": {
      "website": "https://example.com",
      "repo": "https://example.com",
      "appStore": "https://example.com",
      "playStore": "https://example.com",
      "steam": "https://example.com"
    }
  },
  "url": "https://example.com",
  "blockers": [
    "service_missing"
  ],
  "createdAt": "2026-01-01T00:00:00.000Z",
  "updatedAt": "2026-01-01T00:00:00.000Z"
}
```

### An account's published apps as discovery cards (fully-broken listings hidden)

`GET /profiles/{handle}/apps`

**Parameters**

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

**Returns**

- `200` · `PublishedAppCardsPageDto`
- `404` · `ErrorEnvelopeDto` — not_found

Attributes of `PublishedAppCardsPageDto`:

- `items` · `object[]` · read-only
- `nextCursor` · `string | null` · read-only

**Example request**

```bash
curl https://api.koo.io/profiles/:handle/apps \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```json
{
  "items": [
    {
      "id": "acct_01example0000000000000000x",
      "accountId": "acct_01example0000000000000000x",
      "handle": "alice",
      "slug": "my-app",
      "name": "my-app",
      "tagline": "example",
      "photoUrl": "https://example.com",
      "youtubeId": "youtube_01example0000000000000000x",
      "videoCover": true,
      "category": "example",
      "tags": [
        "example"
      ],
      "region": "example",
      "score": 1,
      "createdAt": "2026-01-01T00:00:00.000Z"
    }
  ],
  "nextCursor": "example"
}
```

### Public app detail — profile, images, derived Visit URL, comments (depth ≤ 1), vote/follow state (when authed)

`GET /profiles/{handle}/apps/{slug}`

**Parameters**

- `slug` _(path)_ · `string` · required
- `handle` _(path)_ · `string` · required

**Returns**

- `200` · `PublicAppDto`
- `404` · `ErrorEnvelopeDto` — not_found

Attributes of `PublicAppDto`:

- `id` · `string` · read-only — Identifier of the published app.
- `accountId` · `string` · read-only — Identifier of the owning account.
- `slug` · `string` · read-only — URL slug of the app under the owner's handle.
- `name` · `string` · read-only — Display name of the app (profile displayName, falling back to the slug).
- `author` · `object` · read-only
- `profile` · `object` · read-only
- `url` · `string <uri> | null` · read-only — The public URL to visit, or null when the listing is degraded (the Visit CTA is then hidden).
- `region` · `string | null` · read-only — Primary deploy region — the identifier of the app's exposed service's first enabled location (e.g. `aws-us-east-2`); null when the service or its region can no longer be resolved.
- `counts` · `object` · read-only — Aggregate social counts for the app.
- `comments` · `object` · read-only — The first page of the app's threaded comments (newest first; replies nest one level via parentId).
- `viewerVote` · `-1 | 1` · read-only — The viewing account's current vote (+1 up, -1 down) — present only on an authenticated read with a vote cast.
- `authorFollowState` · `object` · read-only — The viewing account's follow relationship to the author — present only on an authenticated read.
- `createdAt` · `string <date-time>` · read-only — When the app was published.

**Example request**

```bash
curl https://api.koo.io/profiles/:handle/apps/:slug \
  -H "Authorization: Bearer kc_your_api_token"
```

**Example response** (`200`)

```json
{
  "id": "acct_01example0000000000000000x",
  "accountId": "acct_01example0000000000000000x",
  "slug": "my-app",
  "name": "my-app",
  "author": {
    "id": "acct_01example0000000000000000x",
    "handle": "alice",
    "displayName": "My app",
    "avatarUrl": "https://example.com"
  },
  "profile": {
    "displayName": "My app",
    "tagline": "example",
    "description": "example",
    "images": [
      "https://example.com"
    ],
    "youtubeId": "youtube_01example0000000000000000x",
    "videoCover": true,
    "category": "example",
    "tags": [
      "example"
    ],
    "links": {
      "website": "https://example.com",
      "repo": "https://example.com",
      "appStore": "https://example.com",
      "playStore": "https://example.com",
      "steam": "https://example.com"
    }
  },
  "url": "https://example.com",
  "region": "example",
  "counts": {
    "comments": 0,
    "score": 1
  },
  "comments": {
    "data": [
      {
        "id": "acct_01example0000000000000000x",
        "author": {
          "id": "acct_01example0000000000000000x",
          "handle": "alice",
          "displayName": "My app",
          "avatarUrl": "https://example.com"
        },
        "body": "example",
        "score": 1,
        "viewerVote": -1,
        "parentId": "parent_01example0000000000000000x",
        "createdAt": "2026-01-01T00:00:00.000Z",
        "deletedAt": "2026-01-01T00:00:00.000Z",
        "replies": {
          "data": [
            {
              "id": null,
              "author": null,
              "body": null,
              "score": null,
              "viewerVote": null,
              "parentId": null,
              "createdAt": null,
              "deletedAt": null
            }
          ],
          "page": {
            "nextCursor": "example",
            "hasMore": true
          }
        },
        "replyCount": 0
      }
    ],
    "page": {
      "nextCursor": "example",
      "hasMore": true
    }
  },
  "viewerVote": -1,
  "authorFollowState": {
    "following": true,
    "followedBy": true
  },
  "createdAt": "2026-01-01T00:00:00.000Z"
}
```
