Sending domains
Orgs verify their own sending domains; Cowliss registers the identity in AWS SES and mirrors the status SES reports. Claims are exact-string and global: a parent or child of a claimed domain conflicts (409). The dnsRecords array on the DTO is the copy-paste table your DNS must carry (three DKIM CNAMEs, the MX return path and its SPF TXT, plus a recommended DMARC TXT), each with its own status. SES re-checks DNS on its own schedule and emits no domain events, so status refreshes are pull-only. Orgs without a verified domain send from the zero-config shared fallback domain. All mutations require org:admin.
GET /v1/domains
Section titled “GET /v1/domains”List sending domains
Auth: Clerk session token.
Query parameters
{ "cursor": "string", "limit": 50, "q": "string"}| Field | Type | Required | Constraints |
|---|---|---|---|
cursor |
string | no | |
limit |
integer | no | default 50; 1 ≤ x ≤ 100 |
q |
string | no | length 0–200 |
Response 200
{ "data": [ { "id": "string", "orgId": "string", "domain": "string", "region": "string", "status": "not_started", "dkimStatus": "string", "spfStatus": "string", "clickTracking": true, "dnsRecords": [ { "type": "CNAME", "name": "string", "value": "string", "ttl": "string", "priority": "string", "status": "not_started", "recommended": true } ], "lastCheckedAt": "2026-01-15T09:30:00.000Z", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z" } ], "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object[] | yes | |
data[] |
object | yes | |
data[].id |
string | yes | |
data[].orgId |
string | yes | |
data[].domain |
string | yes | |
data[].region |
string | yes | |
data[].status |
“not_started” | “pending” | “success” | “failed” | “temporary_failure” | yes | |
data[].dkimStatus |
string | null | yes | |
data[].spfStatus |
string | null | yes | |
data[].clickTracking |
boolean | yes | |
data[].dnsRecords |
object[] | yes | |
data[].dnsRecords[] |
object | yes | |
data[].dnsRecords[].type |
“CNAME” | “MX” | “TXT” | yes | |
data[].dnsRecords[].name |
string | yes | |
data[].dnsRecords[].value |
string | yes | |
data[].dnsRecords[].ttl |
string | yes | |
data[].dnsRecords[].priority |
string | null | no | |
data[].dnsRecords[].status |
“not_started” | “pending” | “success” | “failed” | “temporary_failure” | yes | |
data[].dnsRecords[].recommended |
boolean | no | |
data[].lastCheckedAt |
string (date-time) | null | yes | |
data[].createdAt |
string (date-time) | yes | |
data[].updatedAt |
string (date-time) | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no |
Errors follow the shared envelope and code table.
GET /v1/domains/{id}
Section titled “GET /v1/domains/{id}”Fetch one sending domain
Auth: Clerk session token.
Response 200
{ "data": { "id": "string", "orgId": "string", "domain": "string", "region": "string", "status": "not_started", "dkimStatus": "string", "spfStatus": "string", "clickTracking": true, "dnsRecords": [ { "type": "CNAME", "name": "string", "value": "string", "ttl": "string", "priority": "string", "status": "not_started", "recommended": true } ], "lastCheckedAt": "2026-01-15T09:30:00.000Z", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.orgId |
string | yes | |
data.domain |
string | yes | |
data.region |
string | yes | |
data.status |
“not_started” | “pending” | “success” | “failed” | “temporary_failure” | yes | |
data.dkimStatus |
string | null | yes | |
data.spfStatus |
string | null | yes | |
data.clickTracking |
boolean | yes | |
data.dnsRecords |
object[] | yes | |
data.dnsRecords[] |
object | yes | |
data.dnsRecords[].type |
“CNAME” | “MX” | “TXT” | yes | |
data.dnsRecords[].name |
string | yes | |
data.dnsRecords[].value |
string | yes | |
data.dnsRecords[].ttl |
string | yes | |
data.dnsRecords[].priority |
string | null | no | |
data.dnsRecords[].status |
“not_started” | “pending” | “success” | “failed” | “temporary_failure” | yes | |
data.dnsRecords[].recommended |
boolean | no | |
data.lastCheckedAt |
string (date-time) | null | yes | |
data.createdAt |
string (date-time) | yes | |
data.updatedAt |
string (date-time) | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no |
Errors follow the shared envelope and code table.
POST /v1/domains
Section titled “POST /v1/domains”Claim a sending domain
Auth: Clerk session token.
Request body
{ "data": { "domain": "string" }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.domain |
string | yes | length 0–253 |
Response 201
{ "data": { "id": "string", "orgId": "string", "domain": "string", "region": "string", "status": "not_started", "dkimStatus": "string", "spfStatus": "string", "clickTracking": true, "dnsRecords": [ { "type": "CNAME", "name": "string", "value": "string", "ttl": "string", "priority": "string", "status": "not_started", "recommended": true } ], "lastCheckedAt": "2026-01-15T09:30:00.000Z", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.orgId |
string | yes | |
data.domain |
string | yes | |
data.region |
string | yes | |
data.status |
“not_started” | “pending” | “success” | “failed” | “temporary_failure” | yes | |
data.dkimStatus |
string | null | yes | |
data.spfStatus |
string | null | yes | |
data.clickTracking |
boolean | yes | |
data.dnsRecords |
object[] | yes | |
data.dnsRecords[] |
object | yes | |
data.dnsRecords[].type |
“CNAME” | “MX” | “TXT” | yes | |
data.dnsRecords[].name |
string | yes | |
data.dnsRecords[].value |
string | yes | |
data.dnsRecords[].ttl |
string | yes | |
data.dnsRecords[].priority |
string | null | no | |
data.dnsRecords[].status |
“not_started” | “pending” | “success” | “failed” | “temporary_failure” | yes | |
data.dnsRecords[].recommended |
boolean | no | |
data.lastCheckedAt |
string (date-time) | null | yes | |
data.createdAt |
string (date-time) | yes | |
data.updatedAt |
string (date-time) | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no |
Errors follow the shared envelope and code table.
POST /v1/domains/{id}/verify
Section titled “POST /v1/domains/{id}/verify”Re-check DNS now
Auth: Clerk session token.
Response 200
{ "data": { "id": "string", "orgId": "string", "domain": "string", "region": "string", "status": "not_started", "dkimStatus": "string", "spfStatus": "string", "clickTracking": true, "dnsRecords": [ { "type": "CNAME", "name": "string", "value": "string", "ttl": "string", "priority": "string", "status": "not_started", "recommended": true } ], "lastCheckedAt": "2026-01-15T09:30:00.000Z", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.orgId |
string | yes | |
data.domain |
string | yes | |
data.region |
string | yes | |
data.status |
“not_started” | “pending” | “success” | “failed” | “temporary_failure” | yes | |
data.dkimStatus |
string | null | yes | |
data.spfStatus |
string | null | yes | |
data.clickTracking |
boolean | yes | |
data.dnsRecords |
object[] | yes | |
data.dnsRecords[] |
object | yes | |
data.dnsRecords[].type |
“CNAME” | “MX” | “TXT” | yes | |
data.dnsRecords[].name |
string | yes | |
data.dnsRecords[].value |
string | yes | |
data.dnsRecords[].ttl |
string | yes | |
data.dnsRecords[].priority |
string | null | no | |
data.dnsRecords[].status |
“not_started” | “pending” | “success” | “failed” | “temporary_failure” | yes | |
data.dnsRecords[].recommended |
boolean | no | |
data.lastCheckedAt |
string (date-time) | null | yes | |
data.createdAt |
string (date-time) | yes | |
data.updatedAt |
string (date-time) | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no |
Errors follow the shared envelope and code table.
PATCH /v1/domains/{id}
Section titled “PATCH /v1/domains/{id}”Toggle click tracking
Auth: Clerk session token.
Request body
{ "data": { "clickTracking": true }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.clickTracking |
boolean | yes |
Response 200
{ "data": { "id": "string", "orgId": "string", "domain": "string", "region": "string", "status": "not_started", "dkimStatus": "string", "spfStatus": "string", "clickTracking": true, "dnsRecords": [ { "type": "CNAME", "name": "string", "value": "string", "ttl": "string", "priority": "string", "status": "not_started", "recommended": true } ], "lastCheckedAt": "2026-01-15T09:30:00.000Z", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-01-15T09:30:00.000Z" }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.id |
string | yes | |
data.orgId |
string | yes | |
data.domain |
string | yes | |
data.region |
string | yes | |
data.status |
“not_started” | “pending” | “success” | “failed” | “temporary_failure” | yes | |
data.dkimStatus |
string | null | yes | |
data.spfStatus |
string | null | yes | |
data.clickTracking |
boolean | yes | |
data.dnsRecords |
object[] | yes | |
data.dnsRecords[] |
object | yes | |
data.dnsRecords[].type |
“CNAME” | “MX” | “TXT” | yes | |
data.dnsRecords[].name |
string | yes | |
data.dnsRecords[].value |
string | yes | |
data.dnsRecords[].ttl |
string | yes | |
data.dnsRecords[].priority |
string | null | no | |
data.dnsRecords[].status |
“not_started” | “pending” | “success” | “failed” | “temporary_failure” | yes | |
data.dnsRecords[].recommended |
boolean | no | |
data.lastCheckedAt |
string (date-time) | null | yes | |
data.createdAt |
string (date-time) | yes | |
data.updatedAt |
string (date-time) | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no |
Errors follow the shared envelope and code table.
DELETE /v1/domains/{id}
Section titled “DELETE /v1/domains/{id}”Release a domain claim
Auth: Clerk session token.
Response 200
{ "data": { "deleted": true }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true }}| Field | Type | Required | Constraints |
|---|---|---|---|
data |
object | yes | |
data.deleted |
true | yes | |
meta |
object | no | |
meta.requestId |
string | no | |
meta.nextCursor |
string | null | no | |
meta.hasMore |
boolean | no |
Errors follow the shared envelope and code table.