# Apps

> Part of the Cowliss docs. Markdown for every page: append `index.md` to its URL. Site map: /llms.txt

<!-- GENERATED by apps/docs/scripts/generate-api-reference.ts. Do not edit. Regenerate with `pnpm -F cowliss-docs generate`. -->

An app is the attribution unit for one product: it carries no kind, configuration, or credential of its own, and each event and profile carries its `appId`. Inbound pipes into an app are sources created under it, and all of an app's sources land on the same profiles. This includes the first-party `api` source created with the app, which the SDK and HTTP path write through. Routes that address a source by its own id (including the public webhook receiver) are on the [Sources](/reference/api/sources/) page. See the [Apps and sources guide](/tracking/apps/).

## POST /v1/apps

Create an app

**Auth:** Session token.

**Request body**

```json
{
  "data": {
    "name": "string"
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.name` | string | yes | length 1–100 |

**Response 201**

```json
{
  "data": {
    "id": "string",
    "orgId": "string",
    "name": "string",
    "publicName": "string",
    "returnUrl": "string",
    "unsubscribeUrlTrait": "string",
    "status": "active",
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-01-15T09:30:00.000Z",
    "archivedAt": "2026-01-15T09:30:00.000Z",
    "events": [
      "string"
    ],
    "eventsSource": "push",
    "sourceCount": 42,
    "lastReceivedAt": "2026-01-15T09:30:00.000Z",
    "automationCount": 42,
    "templateCount": 42
  },
  "meta": {
    "requestId": "string",
    "nextCursor": "string",
    "hasMore": true,
    "warnings": [
      {
        "code": "string",
        "message": "string",
        "suggestion": "string"
      }
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.id` | string | yes |  |
| `data.orgId` | string | yes |  |
| `data.name` | string | yes |  |
| `data.publicName` | string \| null | yes |  |
| `data.returnUrl` | string \| null | yes |  |
| `data.unsubscribeUrlTrait` | string \| null | yes |  |
| `data.status` | "active" \| "archived" | yes |  |
| `data.createdAt` | string (date-time) | yes |  |
| `data.updatedAt` | string (date-time) | yes |  |
| `data.archivedAt` | string (date-time) \| null | yes |  |
| `data.events` | string[] \| null | yes |  |
| `data.eventsSource` | "push" \| "api" | yes |  |
| `data.sourceCount` | integer | yes |  |
| `data.lastReceivedAt` | string (date-time) \| null | yes |  |
| `data.automationCount` | integer | yes |  |
| `data.templateCount` | integer | yes |  |
| `meta` | object | no |  |
| `meta.requestId` | string | no |  |
| `meta.nextCursor` | string \| null | no |  |
| `meta.hasMore` | boolean | no |  |
| `meta.warnings` | object[] | no |  |
| `meta.warnings[]` | object | no |  |
| `meta.warnings[].code` | string | yes |  |
| `meta.warnings[].message` | string | yes |  |
| `meta.warnings[].suggestion` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## GET /v1/apps

List apps

**Auth:** Session token.

**Query parameters**

```json
{
  "cursor": "string",
  "limit": 50,
  "q": "string",
  "status": "all"
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `cursor` | string | no |  |
| `limit` | integer | no | default `50`; 1 ≤ x ≤ 100 |
| `q` | string | no | length 0–200 |
| `status` | "active" \| "archived" \| "all" | no | default `"all"` |

**Response 200**

```json
{
  "data": [
    {
      "id": "string",
      "orgId": "string",
      "name": "string",
      "publicName": "string",
      "returnUrl": "string",
      "unsubscribeUrlTrait": "string",
      "status": "active",
      "createdAt": "2026-01-15T09:30:00.000Z",
      "updatedAt": "2026-01-15T09:30:00.000Z",
      "archivedAt": "2026-01-15T09:30:00.000Z",
      "events": [
        "string"
      ],
      "eventsSource": "push",
      "sourceCount": 42,
      "lastReceivedAt": "2026-01-15T09:30:00.000Z",
      "automationCount": 42,
      "templateCount": 42
    }
  ],
  "meta": {
    "requestId": "string",
    "nextCursor": "string",
    "hasMore": true,
    "warnings": [
      {
        "code": "string",
        "message": "string",
        "suggestion": "string"
      }
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object[] | yes |  |
| `data[]` | object | yes |  |
| `data[].id` | string | yes |  |
| `data[].orgId` | string | yes |  |
| `data[].name` | string | yes |  |
| `data[].publicName` | string \| null | yes |  |
| `data[].returnUrl` | string \| null | yes |  |
| `data[].unsubscribeUrlTrait` | string \| null | yes |  |
| `data[].status` | "active" \| "archived" | yes |  |
| `data[].createdAt` | string (date-time) | yes |  |
| `data[].updatedAt` | string (date-time) | yes |  |
| `data[].archivedAt` | string (date-time) \| null | yes |  |
| `data[].events` | string[] \| null | yes |  |
| `data[].eventsSource` | "push" \| "api" | yes |  |
| `data[].sourceCount` | integer | yes |  |
| `data[].lastReceivedAt` | string (date-time) \| null | yes |  |
| `data[].automationCount` | integer | yes |  |
| `data[].templateCount` | integer | yes |  |
| `meta` | object | no |  |
| `meta.requestId` | string | no |  |
| `meta.nextCursor` | string \| null | no |  |
| `meta.hasMore` | boolean | no |  |
| `meta.warnings` | object[] | no |  |
| `meta.warnings[]` | object | no |  |
| `meta.warnings[].code` | string | yes |  |
| `meta.warnings[].message` | string | yes |  |
| `meta.warnings[].suggestion` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## GET /v1/apps/health

Every live app's health

Each live app the caller reaches, with its six checks and its health (as `apps.health.get`), plus its events and deliveries in the last 24 hours, an hourly series of events over 7 days, its failed rate, its last event and its last push. Problems first, then the busiest. One response, not a page.

**Auth:** Session token, or an org [pipeline key](/guides/keys/).

**Response 200**

```json
{
  "data": {
    "apps": [
      {
        "appId": "string",
        "name": "string",
        "sourceId": "string",
        "health": "fail",
        "checks": [
          {
            "code": "events_arriving",
            "status": "pass",
            "label": "string",
            "detail": "string",
            "prompt": "string",
            "names": [
              "string"
            ]
          }
        ],
        "events24h": 42,
        "hourly": [
          42
        ],
        "deliveries24h": 42,
        "failedRate": 42,
        "lastEventAt": "2026-01-15T09:30:00.000Z",
        "lastPushAt": "2026-01-15T09:30:00.000Z"
      }
    ]
  },
  "meta": {
    "requestId": "string",
    "nextCursor": "string",
    "hasMore": true,
    "warnings": [
      {
        "code": "string",
        "message": "string",
        "suggestion": "string"
      }
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.apps` | object[] | yes |  |
| `data.apps[]` | object | yes |  |
| `data.apps[].appId` | string | yes |  |
| `data.apps[].name` | string | yes |  |
| `data.apps[].sourceId` | string \| null | yes |  |
| `data.apps[].health` | "fail" \| "warn" \| "setting_up" \| "quiet" \| "healthy" | yes |  |
| `data.apps[].checks` | object[] | yes |  |
| `data.apps[].checks[]` | object | yes |  |
| `data.apps[].checks[].code` | "events_arriving" \| "key_scoped" \| "code_pushed" \| "sending_domain" \| "automations_on" \| "events_listed" \| "signup_sender" | yes |  |
| `data.apps[].checks[].status` | "pass" \| "warn" \| "fail" \| "not_used" | yes |  |
| `data.apps[].checks[].label` | string | yes |  |
| `data.apps[].checks[].detail` | string | yes |  |
| `data.apps[].checks[].prompt` | string \| null | yes |  |
| `data.apps[].checks[].names` | string[] | no |  |
| `data.apps[].events24h` | integer | yes | 0 ≤ x ≤ ∞ |
| `data.apps[].hourly` | integer[] | yes |  |
| `data.apps[].deliveries24h` | integer | yes | 0 ≤ x ≤ ∞ |
| `data.apps[].failedRate` | number \| null | yes |  |
| `data.apps[].lastEventAt` | string (date-time) \| null | yes |  |
| `data.apps[].lastPushAt` | string (date-time) \| null | yes |  |
| `meta` | object | no |  |
| `meta.requestId` | string | no |  |
| `meta.nextCursor` | string \| null | no |  |
| `meta.hasMore` | boolean | no |  |
| `meta.warnings` | object[] | no |  |
| `meta.warnings[]` | object | no |  |
| `meta.warnings[].code` | string | yes |  |
| `meta.warnings[].message` | string | yes |  |
| `meta.warnings[].suggestion` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## GET /v1/apps/{id}/health

Whether an app is set up and healthy

Seven checks, each with a stable `code`, a `status` (`pass`, `warn`, `fail` or `not_used`), a `label` and a `detail` line, and for a check that does not pass, a `prompt` an agent can act on: `events_arriving` (an event in the last 6 hours, or the app is quiet), `key_scoped` (the last 24 hours of events were signed by keys that reach only this app), `code_pushed` (the last build of every automation and template succeeded), `sending_domain` (every domain an automation sends from is verified), `automations_on` (no automation is on hold), `events_listed` (every name seen since the last push is in cow.json) and `signup_sender` (an app with a list has the default sender its signup confirm emails leave from). `health` rolls them up: `fail`, then `warn`, then `setting_up` (no event ever), `quiet` or `healthy`.

**Auth:** Session token, or an org [pipeline key](/guides/keys/).

**Response 200**

```json
{
  "data": {
    "appId": "string",
    "name": "string",
    "sourceId": "string",
    "health": "fail",
    "checks": [
      {
        "code": "events_arriving",
        "status": "pass",
        "label": "string",
        "detail": "string",
        "prompt": "string",
        "names": [
          "string"
        ]
      }
    ],
    "events24h": 42,
    "hourly": [
      42
    ],
    "deliveries24h": 42,
    "failedRate": 42,
    "lastEventAt": "2026-01-15T09:30:00.000Z",
    "lastPushAt": "2026-01-15T09:30:00.000Z"
  },
  "meta": {
    "requestId": "string",
    "nextCursor": "string",
    "hasMore": true,
    "warnings": [
      {
        "code": "string",
        "message": "string",
        "suggestion": "string"
      }
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.appId` | string | yes |  |
| `data.name` | string | yes |  |
| `data.sourceId` | string \| null | yes |  |
| `data.health` | "fail" \| "warn" \| "setting_up" \| "quiet" \| "healthy" | yes |  |
| `data.checks` | object[] | yes |  |
| `data.checks[]` | object | yes |  |
| `data.checks[].code` | "events_arriving" \| "key_scoped" \| "code_pushed" \| "sending_domain" \| "automations_on" \| "events_listed" \| "signup_sender" | yes |  |
| `data.checks[].status` | "pass" \| "warn" \| "fail" \| "not_used" | yes |  |
| `data.checks[].label` | string | yes |  |
| `data.checks[].detail` | string | yes |  |
| `data.checks[].prompt` | string \| null | yes |  |
| `data.checks[].names` | string[] | no |  |
| `data.events24h` | integer | yes | 0 ≤ x ≤ ∞ |
| `data.hourly` | integer[] | yes |  |
| `data.deliveries24h` | integer | yes | 0 ≤ x ≤ ∞ |
| `data.failedRate` | number \| null | yes |  |
| `data.lastEventAt` | string (date-time) \| null | yes |  |
| `data.lastPushAt` | string (date-time) \| null | yes |  |
| `meta` | object | no |  |
| `meta.requestId` | string | no |  |
| `meta.nextCursor` | string \| null | no |  |
| `meta.hasMore` | boolean | no |  |
| `meta.warnings` | object[] | no |  |
| `meta.warnings[]` | object | no |  |
| `meta.warnings[].code` | string | yes |  |
| `meta.warnings[].message` | string | yes |  |
| `meta.warnings[].suggestion` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## GET /v1/apps/{id}

Fetch one app

**Auth:** Session token.

**Response 200**

```json
{
  "data": {
    "id": "string",
    "orgId": "string",
    "name": "string",
    "publicName": "string",
    "returnUrl": "string",
    "unsubscribeUrlTrait": "string",
    "status": "active",
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-01-15T09:30:00.000Z",
    "archivedAt": "2026-01-15T09:30:00.000Z",
    "events": [
      "string"
    ],
    "eventsSource": "push",
    "sourceCount": 42,
    "lastReceivedAt": "2026-01-15T09:30:00.000Z",
    "automationCount": 42,
    "templateCount": 42
  },
  "meta": {
    "requestId": "string",
    "nextCursor": "string",
    "hasMore": true,
    "warnings": [
      {
        "code": "string",
        "message": "string",
        "suggestion": "string"
      }
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.id` | string | yes |  |
| `data.orgId` | string | yes |  |
| `data.name` | string | yes |  |
| `data.publicName` | string \| null | yes |  |
| `data.returnUrl` | string \| null | yes |  |
| `data.unsubscribeUrlTrait` | string \| null | yes |  |
| `data.status` | "active" \| "archived" | yes |  |
| `data.createdAt` | string (date-time) | yes |  |
| `data.updatedAt` | string (date-time) | yes |  |
| `data.archivedAt` | string (date-time) \| null | yes |  |
| `data.events` | string[] \| null | yes |  |
| `data.eventsSource` | "push" \| "api" | yes |  |
| `data.sourceCount` | integer | yes |  |
| `data.lastReceivedAt` | string (date-time) \| null | yes |  |
| `data.automationCount` | integer | yes |  |
| `data.templateCount` | integer | yes |  |
| `meta` | object | no |  |
| `meta.requestId` | string | no |  |
| `meta.nextCursor` | string \| null | no |  |
| `meta.hasMore` | boolean | no |  |
| `meta.warnings` | object[] | no |  |
| `meta.warnings[]` | object | no |  |
| `meta.warnings[].code` | string | yes |  |
| `meta.warnings[].message` | string | yes |  |
| `meta.warnings[].suggestion` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## PATCH /v1/apps/{id}

Rename or archive an app, set what recipients see of it, or its event list

Admin only. Archiving is permanent: an archived app cannot be unarchived or modified. Renaming changes the display name only; the app id never changes. `publicName` is what recipients see the app called on the pages an unsubscribe link opens, in place of `name`, and `returnUrl` (https only) is where those pages link back to. An empty string or `null` clears either. `unsubscribeUrlTrait` names a profile trait holding the recipient's own unsubscribe URL, which a marketing email then links to in place of Cowliss's unsubscribe; that URL answers GET with a page offering the change and makes it only on the one-click POST (see [Partners](/guides/partners/#your-own-unsubscribe)). An empty string or `null` clears it. `events` replaces the app's event list (the names it sends, as in `cow.json`), and `null` turns the list off; a later push that names a different list asks before overwriting it.

**Auth:** Session token.

**Request body**

```json
{
  "data": {
    "name": "string",
    "status": "archived",
    "publicName": "string",
    "returnUrl": "string",
    "unsubscribeUrlTrait": "string",
    "events": [
      "string"
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.name` | string | no | length 1–100 |
| `data.status` | "archived" | no |  |
| `data.publicName` | string \| null | no |  |
| `data.returnUrl` | string \| null | no |  |
| `data.unsubscribeUrlTrait` | string \| null | no |  |
| `data.events` | string[] \| null | no |  |

**Response 200**

```json
{
  "data": {
    "id": "string",
    "orgId": "string",
    "name": "string",
    "publicName": "string",
    "returnUrl": "string",
    "unsubscribeUrlTrait": "string",
    "status": "active",
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-01-15T09:30:00.000Z",
    "archivedAt": "2026-01-15T09:30:00.000Z",
    "events": [
      "string"
    ],
    "eventsSource": "push",
    "sourceCount": 42,
    "lastReceivedAt": "2026-01-15T09:30:00.000Z",
    "automationCount": 42,
    "templateCount": 42
  },
  "meta": {
    "requestId": "string",
    "nextCursor": "string",
    "hasMore": true,
    "warnings": [
      {
        "code": "string",
        "message": "string",
        "suggestion": "string"
      }
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.id` | string | yes |  |
| `data.orgId` | string | yes |  |
| `data.name` | string | yes |  |
| `data.publicName` | string \| null | yes |  |
| `data.returnUrl` | string \| null | yes |  |
| `data.unsubscribeUrlTrait` | string \| null | yes |  |
| `data.status` | "active" \| "archived" | yes |  |
| `data.createdAt` | string (date-time) | yes |  |
| `data.updatedAt` | string (date-time) | yes |  |
| `data.archivedAt` | string (date-time) \| null | yes |  |
| `data.events` | string[] \| null | yes |  |
| `data.eventsSource` | "push" \| "api" | yes |  |
| `data.sourceCount` | integer | yes |  |
| `data.lastReceivedAt` | string (date-time) \| null | yes |  |
| `data.automationCount` | integer | yes |  |
| `data.templateCount` | integer | yes |  |
| `meta` | object | no |  |
| `meta.requestId` | string | no |  |
| `meta.nextCursor` | string \| null | no |  |
| `meta.hasMore` | boolean | no |  |
| `meta.warnings` | object[] | no |  |
| `meta.warnings[]` | object | no |  |
| `meta.warnings[].code` | string | yes |  |
| `meta.warnings[].message` | string | yes |  |
| `meta.warnings[].suggestion` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## GET /v1/apps/{id}/status

What an app's automations and templates are doing

**Auth:** Session token, or an org [pipeline key](/guides/keys/).

**Response 200**

```json
{
  "data": {
    "appId": "string",
    "keys": [
      {
        "kind": "automation",
        "key": "string",
        "latestVersion": {
          "id": "string",
          "orgId": "string",
          "key": "string",
          "kind": "automation",
          "appId": "string",
          "source": "push",
          "moduleDigest": "string",
          "manifest": {
            "key": "string",
            "tags": {},
            "trigger": {
              "event": "string"
            },
            "purpose": "string",
            "enrollment": {
              "cooldown": "string"
            },
            "description": "string",
            "from": "string",
            "definition": {
              "trigger": {
                "event": "string"
              },
              "purpose": "marketing",
              "from": "string",
              "tags": {},
              "description": "string",
              "enrollment": {
                "cooldown": "string"
              },
              "steps": [
                {
                  "id": "string",
                  "type": "sleep",
                  "duration": "string"
                }
              ]
            },
            "spine": [
              {
                "name": "sleep",
                "detail": "string",
                "timeout": "string",
                "steps": [],
                "otherwise": []
              }
            ],
            "bundle": "string"
          },
          "protocol": 42,
          "status": "compiling",
          "pushedAt": "2026-01-15T09:30:00.000Z",
          "compiledAt": "2026-01-15T09:30:00.000Z",
          "error": "string"
        },
        "status": "on",
        "liveExecutions": 42
      }
    ],
    "warnings": [
      "string"
    ]
  },
  "meta": {
    "requestId": "string",
    "nextCursor": "string",
    "hasMore": true,
    "warnings": [
      {
        "code": "string",
        "message": "string",
        "suggestion": "string"
      }
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.appId` | string | yes | length 0–200 |
| `data.keys` | object[] | yes |  |
| `data.keys[]` | object | yes |  |
| `data.keys[].kind` | "automation" \| "template" | yes |  |
| `data.keys[].key` | string | yes |  |
| `data.keys[].latestVersion` | object \| null | yes |  |
| `data.keys[].latestVersion.id` | string | yes |  |
| `data.keys[].latestVersion.orgId` | string | yes |  |
| `data.keys[].latestVersion.key` | string | yes |  |
| `data.keys[].latestVersion.kind` | "automation" \| "template" | yes |  |
| `data.keys[].latestVersion.appId` | string | yes |  |
| `data.keys[].latestVersion.source` | "push" \| "api" | yes |  |
| `data.keys[].latestVersion.moduleDigest` | string \| null | yes |  |
| `data.keys[].latestVersion.manifest` | object \| object | yes |  |
| `data.keys[].latestVersion.protocol` | integer | yes | -2147483648 ≤ x ≤ 2147483647 |
| `data.keys[].latestVersion.status` | "compiling" \| "ready" \| "failed" | yes |  |
| `data.keys[].latestVersion.pushedAt` | string (date-time) | yes |  |
| `data.keys[].latestVersion.compiledAt` | string (date-time) \| null | yes |  |
| `data.keys[].latestVersion.error` | string \| null | yes |  |
| `data.keys[].status` | "on" \| "off" \| "paused" \| null | yes |  |
| `data.keys[].liveExecutions` | integer | yes |  |
| `data.warnings` | string[] | yes |  |
| `meta` | object | no |  |
| `meta.requestId` | string | no |  |
| `meta.nextCursor` | string \| null | no |  |
| `meta.hasMore` | boolean | no |  |
| `meta.warnings` | object[] | no |  |
| `meta.warnings[]` | object | no |  |
| `meta.warnings[].code` | string | yes |  |
| `meta.warnings[].message` | string | yes |  |
| `meta.warnings[].suggestion` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## GET /v1/apps/{id}/broadcast-settings

An app's broadcast look and default sender

The app's Brand: the logo, colours and font every broadcast, the signup confirm email and the hosted signup and unsubscribe pages render with, the postal address under a broadcast's footer, the `from` and `replyTo` a new broadcast takes when it names none (and the `from` the confirm email leaves from), and the `language` of the confirm email and the hosted pages (`en`, `fr`, `ro` or `es`; English when unset). `logoWidth` and `logoHeight` are the logo's pixel size, read by the API; the logo renders at that size, scaled down to fit 200 by 64. Empty until an admin sets them.

**Auth:** Session token.

**Response 200**

```json
{
  "data": {
    "logoUrl": "https://example.com/hooks/cowliss",
    "colors": {
      "background": "string",
      "text": "string",
      "heading": "string",
      "link": "string",
      "button": "string",
      "buttonText": "string"
    },
    "font": "string",
    "logoWidth": 42,
    "logoHeight": 42,
    "postalAddress": "string",
    "from": "string",
    "replyTo": "ada@example.com",
    "language": "en"
  },
  "meta": {
    "requestId": "string",
    "nextCursor": "string",
    "hasMore": true,
    "warnings": [
      {
        "code": "string",
        "message": "string",
        "suggestion": "string"
      }
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.logoUrl` | string (uri) | no | length 0–2048 |
| `data.colors` | object | no |  |
| `data.colors.background` | string | no |  |
| `data.colors.text` | string | no |  |
| `data.colors.heading` | string | no |  |
| `data.colors.link` | string | no |  |
| `data.colors.button` | string | no |  |
| `data.colors.buttonText` | string | no |  |
| `data.font` | string | no |  |
| `data.logoWidth` | integer | no |  |
| `data.logoHeight` | integer | no |  |
| `data.postalAddress` | string | no | length 1–300 |
| `data.from` | string | no | length 0–200 |
| `data.replyTo` | string (email) | no | length 0–320 |
| `data.language` | "en" \| "fr" \| "ro" \| "es" | no |  |
| `meta` | object | no |  |
| `meta.requestId` | string | no |  |
| `meta.nextCursor` | string \| null | no |  |
| `meta.hasMore` | boolean | no |  |
| `meta.warnings` | object[] | no |  |
| `meta.warnings[]` | object | no |  |
| `meta.warnings[].code` | string | yes |  |
| `meta.warnings[].message` | string | yes |  |
| `meta.warnings[].suggestion` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## PUT /v1/apps/{id}/broadcast-settings

Set an app's broadcast look and default sender

Admin only. Replaces the whole of it: a field left out is cleared. `from` must be on a verified sending domain. `logoWidth` and `logoHeight` are ignored: the API keeps them while `logoUrl` is unchanged, reads them from an uploaded logo, and otherwise fetches a changed `logoUrl` once and reads them from the image, refusing it unless it is a png, jpg, webp or gif at a public address.

**Auth:** Session token.

**Request body**

```json
{
  "data": {
    "logoUrl": "https://example.com/hooks/cowliss",
    "colors": {
      "background": "string",
      "text": "string",
      "heading": "string",
      "link": "string",
      "button": "string",
      "buttonText": "string"
    },
    "font": "string",
    "logoWidth": 42,
    "logoHeight": 42,
    "postalAddress": "string",
    "from": "string",
    "replyTo": "ada@example.com",
    "language": "en"
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.logoUrl` | string (uri) | no | length 0–2048 |
| `data.colors` | object | no |  |
| `data.colors.background` | string | no |  |
| `data.colors.text` | string | no |  |
| `data.colors.heading` | string | no |  |
| `data.colors.link` | string | no |  |
| `data.colors.button` | string | no |  |
| `data.colors.buttonText` | string | no |  |
| `data.font` | string | no |  |
| `data.logoWidth` | integer | no |  |
| `data.logoHeight` | integer | no |  |
| `data.postalAddress` | string | no | length 1–300 |
| `data.from` | string | no | length 0–200 |
| `data.replyTo` | string (email) | no | length 0–320 |
| `data.language` | "en" \| "fr" \| "ro" \| "es" | no |  |

**Response 200**

```json
{
  "data": {
    "logoUrl": "https://example.com/hooks/cowliss",
    "colors": {
      "background": "string",
      "text": "string",
      "heading": "string",
      "link": "string",
      "button": "string",
      "buttonText": "string"
    },
    "font": "string",
    "logoWidth": 42,
    "logoHeight": 42,
    "postalAddress": "string",
    "from": "string",
    "replyTo": "ada@example.com",
    "language": "en"
  },
  "meta": {
    "requestId": "string",
    "nextCursor": "string",
    "hasMore": true,
    "warnings": [
      {
        "code": "string",
        "message": "string",
        "suggestion": "string"
      }
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.logoUrl` | string (uri) | no | length 0–2048 |
| `data.colors` | object | no |  |
| `data.colors.background` | string | no |  |
| `data.colors.text` | string | no |  |
| `data.colors.heading` | string | no |  |
| `data.colors.link` | string | no |  |
| `data.colors.button` | string | no |  |
| `data.colors.buttonText` | string | no |  |
| `data.font` | string | no |  |
| `data.logoWidth` | integer | no |  |
| `data.logoHeight` | integer | no |  |
| `data.postalAddress` | string | no | length 1–300 |
| `data.from` | string | no | length 0–200 |
| `data.replyTo` | string (email) | no | length 0–320 |
| `data.language` | "en" \| "fr" \| "ro" \| "es" | no |  |
| `meta` | object | no |  |
| `meta.requestId` | string | no |  |
| `meta.nextCursor` | string \| null | no |  |
| `meta.hasMore` | boolean | no |  |
| `meta.warnings` | object[] | no |  |
| `meta.warnings[]` | object | no |  |
| `meta.warnings[].code` | string | yes |  |
| `meta.warnings[].message` | string | yes |  |
| `meta.warnings[].suggestion` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## POST /v1/apps/{id}/logo

Upload a logo for an app's broadcasts

Admin only. Stores the image and answers with the https `url` it is served at; pass that to `apps.look.set` as `logoUrl`, which this does not do itself. `content` is the image base64-encoded: a png, jpg, webp or gif (told by its bytes, never by a name or a declared type; svg is refused) of at most 1 MB. Every upload gets a new address, so replacing a logo never shows the old one from a cache. Answers `dependency_unavailable` where uploads are not set up.

**Auth:** Session token.

**Request body**

```json
{
  "data": {
    "content": "string"
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.content` | string (base64) | yes | length 1–∞ |

**Response 201**

```json
{
  "data": {
    "url": "https://example.com/hooks/cowliss",
    "width": 42,
    "height": 42
  },
  "meta": {
    "requestId": "string",
    "nextCursor": "string",
    "hasMore": true,
    "warnings": [
      {
        "code": "string",
        "message": "string",
        "suggestion": "string"
      }
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.url` | string (uri) | yes |  |
| `data.width` | integer | yes |  |
| `data.height` | integer | yes |  |
| `meta` | object | no |  |
| `meta.requestId` | string | no |  |
| `meta.nextCursor` | string \| null | no |  |
| `meta.hasMore` | boolean | no |  |
| `meta.warnings` | object[] | no |  |
| `meta.warnings[]` | object | no |  |
| `meta.warnings[].code` | string | yes |  |
| `meta.warnings[].message` | string | yes |  |
| `meta.warnings[].suggestion` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## POST /v1/apps/{appId}/sources

Add a source to an app

Admin only. An `api` source takes a `name`, unique among the app's active sources; an app may hold any number of them. A `clerk` source is named after its kind, and an app holds one active at a time.

**Auth:** Session token.

**Request body**

```json
{
  "data": {
    "kind": "api",
    "name": "string",
    "config": {}
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | one of | yes |  |
| `data〈api〉` | object | yes |  |
| `data〈api〉.kind` | "api" | yes |  |
| `data〈api〉.name` | string | yes | length 1–100 |
| `data〈api〉.config` | object | no | default `{}` |
| `data〈clerk〉` | object | yes |  |
| `data〈clerk〉.kind` | "clerk" | yes |  |
| `data〈clerk〉.config` | object | no | default `{}` |
| `data〈clerk〉.config.signingSecret` | string | no | length 1–∞ |

**Response 201**

```json
{
  "data": {
    "id": "string",
    "orgId": "string",
    "appId": "string",
    "kind": "api",
    "name": "string",
    "config": "string",
    "lastReceivedAt": "2026-01-15T09:30:00.000Z",
    "createdAt": "2026-01-15T09:30:00.000Z",
    "updatedAt": "2026-01-15T09:30:00.000Z",
    "deactivatedAt": "2026-01-15T09:30:00.000Z",
    "status": "active",
    "configured": true
  },
  "meta": {
    "requestId": "string",
    "nextCursor": "string",
    "hasMore": true,
    "warnings": [
      {
        "code": "string",
        "message": "string",
        "suggestion": "string"
      }
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.id` | string | yes |  |
| `data.orgId` | string | yes |  |
| `data.appId` | string | yes |  |
| `data.kind` | "api" \| "clerk" | yes |  |
| `data.name` | string | yes |  |
| `data.config` | string \| number \| boolean \| null \| record\<string, any> \| any[] | yes |  |
| `data.lastReceivedAt` | string (date-time) \| null | yes |  |
| `data.createdAt` | string (date-time) | yes |  |
| `data.updatedAt` | string (date-time) | yes |  |
| `data.deactivatedAt` | string (date-time) \| null | yes |  |
| `data.status` | "active" \| "inactive" | yes |  |
| `data.configured` | boolean | yes |  |
| `meta` | object | no |  |
| `meta.requestId` | string | no |  |
| `meta.nextCursor` | string \| null | no |  |
| `meta.hasMore` | boolean | no |  |
| `meta.warnings` | object[] | no |  |
| `meta.warnings[]` | object | no |  |
| `meta.warnings[].code` | string | yes |  |
| `meta.warnings[].message` | string | yes |  |
| `meta.warnings[].suggestion` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).

## GET /v1/apps/{appId}/sources

List an app's sources

**Auth:** Session token.

**Query parameters**

```json
{
  "cursor": "string",
  "limit": 50
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `cursor` | string | no |  |
| `limit` | integer | no | default `50`; 1 ≤ x ≤ 100 |

**Response 200**

```json
{
  "data": [
    {
      "id": "string",
      "orgId": "string",
      "appId": "string",
      "kind": "api",
      "name": "string",
      "config": "string",
      "lastReceivedAt": "2026-01-15T09:30:00.000Z",
      "createdAt": "2026-01-15T09:30:00.000Z",
      "updatedAt": "2026-01-15T09:30:00.000Z",
      "deactivatedAt": "2026-01-15T09:30:00.000Z",
      "status": "active",
      "configured": true
    }
  ],
  "meta": {
    "requestId": "string",
    "nextCursor": "string",
    "hasMore": true,
    "warnings": [
      {
        "code": "string",
        "message": "string",
        "suggestion": "string"
      }
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object[] | yes |  |
| `data[]` | object | yes |  |
| `data[].id` | string | yes |  |
| `data[].orgId` | string | yes |  |
| `data[].appId` | string | yes |  |
| `data[].kind` | "api" \| "clerk" | yes |  |
| `data[].name` | string | yes |  |
| `data[].config` | string \| number \| boolean \| null \| record\<string, any> \| any[] | yes |  |
| `data[].lastReceivedAt` | string (date-time) \| null | yes |  |
| `data[].createdAt` | string (date-time) | yes |  |
| `data[].updatedAt` | string (date-time) | yes |  |
| `data[].deactivatedAt` | string (date-time) \| null | yes |  |
| `data[].status` | "active" \| "inactive" | yes |  |
| `data[].configured` | boolean | yes |  |
| `meta` | object | no |  |
| `meta.requestId` | string | no |  |
| `meta.nextCursor` | string \| null | no |  |
| `meta.hasMore` | boolean | no |  |
| `meta.warnings` | object[] | no |  |
| `meta.warnings[]` | object | no |  |
| `meta.warnings[].code` | string | yes |  |
| `meta.warnings[].message` | string | yes |  |
| `meta.warnings[].suggestion` | string | no |  |

Errors follow the [shared envelope and code table](/api-conventions/#errors).
