Lists
A list is a marketing.<name> consent purpose 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
Section titled “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
{ "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
{ "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.
GET /v1/apps/{id}/lists
Section titled “GET /v1/apps/{id}/lists”List an app’s lists
Auth: Session token.
Query parameters
{ "cursor": "string", "limit": 50}| Field | Type | Required | Constraints |
|---|---|---|---|
cursor |
string | no | |
limit |
integer | no | default 50; 1 ≤ x ≤ 100 |
Response 200
{ "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.
GET /v1/apps/{id}/lists/{key}/subscribers
Section titled “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
{ "cursor": "string", "limit": 50}| Field | Type | Required | Constraints |
|---|---|---|---|
cursor |
string | no | |
limit |
integer | no | default 50; 1 ≤ x ≤ 100 |
Response 200
{ "data": [ { "profileId": "string", "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.
POST /v1/apps/{id}/lists/{key}/imports
Section titled “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
{ "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.