Apps
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 page. See the Apps and sources guide.
POST /v1/apps
Section titled “POST /v1/apps”Create an app
Auth: Session token.
Request body
{ "data": { "name": "string" }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.name |
string | yes | length 1–100 |
Response 201
{ "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.
GET /v1/apps
Section titled “GET /v1/apps”List apps
Auth: Session token.
Query parameters
{ "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
{ "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.
GET /v1/apps/health
Section titled “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.
Response 200
{ "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.
GET /v1/apps/{id}/health
Section titled “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.
Response 200
{ "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.
GET /v1/apps/{id}
Section titled “GET /v1/apps/{id}”Fetch one app
Auth: Session token.
Response 200
{ "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.
PATCH /v1/apps/{id}
Section titled “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). 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
{ "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
{ "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.
GET /v1/apps/{id}/status
Section titled “GET /v1/apps/{id}/status”What an app’s automations and templates are doing
Auth: Session token, or an org pipeline key.
Response 200
{ "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.
GET /v1/apps/{id}/broadcast-settings
Section titled “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
{ "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", "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.
PUT /v1/apps/{id}/broadcast-settings
Section titled “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
{ "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", "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
{ "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", "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.
POST /v1/apps/{id}/logo
Section titled “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
{ "data": { "content": "string" }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.content |
string (base64) | yes | length 1–∞ |
Response 201
{ "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.
POST /v1/apps/{appId}/sources
Section titled “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
{ "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
{ "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.
GET /v1/apps/{appId}/sources
Section titled “GET /v1/apps/{appId}/sources”List an app’s sources
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": [ { "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.