# Lists

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

A list is a `marketing.<name>` [consent purpose](/reference/api/consent/) of one app: its subscribers are the app's profiles holding that purpose granted, and there is no second list beside the purpose. A list is declared in `cow.json` or created here; a push adds and relabels purposes and never deletes one, so a list made in the dashboard survives every push. Subscribers join through your app's `identify` with a consent grant, a CSV import, or a hosted signup form that confirms by email. All routes require session-token authentication, and creating a list requires `org:admin`.

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

Create a list

Creates the `marketing.<name>` purpose on the app, written by the API. A later push declaring the same key with another label asks before taking it over; `consent.purposes.delete` removes it once nothing sends to it and nobody is subscribed. Refused when the app already has the key.

**Auth:** Session token.

**Request body**

```json
{
  "data": {
    "name": "string",
    "label": "string",
    "description": "string"
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.name` | string | yes | length 0–60 |
| `data.label` | string | yes | length 1–120 |
| `data.description` | string | no | length 0–500 |

**Response 201**

```json
{
  "data": {
    "key": "string",
    "name": "string",
    "label": "string",
    "description": "string",
    "source": "push",
    "subscribers": 42,
    "change": 42,
    "subscribedLast7d": 42,
    "lastSentAt": "string",
    "signupToken": "string",
    "signupUrl": "string"
  },
  "meta": {
    "requestId": "string",
    "nextCursor": "string",
    "hasMore": true,
    "warnings": [
      {
        "code": "string",
        "message": "string",
        "suggestion": "string"
      }
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.key` | string | yes |  |
| `data.name` | string | yes |  |
| `data.label` | string | yes |  |
| `data.description` | string \| null | yes |  |
| `data.source` | "push" \| "api" | yes |  |
| `data.subscribers` | integer | yes |  |
| `data.change` | integer | yes |  |
| `data.subscribedLast7d` | integer | yes |  |
| `data.lastSentAt` | string \| null | yes |  |
| `data.signupToken` | string | yes |  |
| `data.signupUrl` | 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}/lists

List an app's lists

**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": [
    {
      "key": "string",
      "name": "string",
      "label": "string",
      "description": "string",
      "source": "push",
      "subscribers": 42,
      "change": 42,
      "subscribedLast7d": 42,
      "lastSentAt": "string",
      "signupToken": "string",
      "signupUrl": "string"
    }
  ],
  "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[].key` | string | yes |  |
| `data[].name` | string | yes |  |
| `data[].label` | string | yes |  |
| `data[].description` | string \| null | yes |  |
| `data[].source` | "push" \| "api" | yes |  |
| `data[].subscribers` | integer | yes |  |
| `data[].change` | integer | yes |  |
| `data[].subscribedLast7d` | integer | yes |  |
| `data[].lastSentAt` | string \| null | yes |  |
| `data[].signupToken` | string | yes |  |
| `data[].signupUrl` | 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}/lists/{key}/subscribers

List a list's subscribers

The app's profiles holding the purpose granted, newest profile first, with when and how each joined.

**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": [
    {
      "profileId": "string",
      "email": "ada@example.com",
      "joinedAt": "string",
      "source": "app",
      "lastOpenedAt": "string"
    }
  ],
  "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[].profileId` | string | yes |  |
| `data[].email` | string \| null | yes |  |
| `data[].joinedAt` | string \| null | yes |  |
| `data[].source` | "app" \| "imported" \| "form" | yes |  |
| `data[].lastOpenedAt` | string \| 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).

## POST /v1/apps/{id}/lists/{key}/imports

Import a CSV into a list

A `multipart/form-data` upload: the CSV as `file` and `attested` set to `true`, the importer's confirmation that these people agreed to receive this list. Matches or creates a user by the `email` identifier for every row and grants the list with consent reason `imported`. Quiet: no automation starts and no email is sent. A suppressed address is still imported and counted. Re-importing a file changes nothing. At most 5 MB and 10000 rows.

**Auth:** Session token.

**Response 200**

```json
{
  "data": {
    "added": 42,
    "alreadySubscribed": 42,
    "unsubscribed": 42,
    "suppressed": 42,
    "invalid": [
      {
        "line": 42,
        "reason": "string"
      }
    ]
  },
  "meta": {
    "requestId": "string",
    "nextCursor": "string",
    "hasMore": true,
    "warnings": [
      {
        "code": "string",
        "message": "string",
        "suggestion": "string"
      }
    ]
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.added` | integer | yes |  |
| `data.alreadySubscribed` | integer | yes |  |
| `data.unsubscribed` | integer | yes |  |
| `data.suppressed` | integer | yes |  |
| `data.invalid` | object[] | yes |  |
| `data.invalid[]` | object | yes |  |
| `data.invalid[].line` | integer | yes |  |
| `data.invalid[].reason` | 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).
