Sending domains
An organization cannot send without a verified domain. Each name is claimed on its own (acme.com and mail.acme.com are separate claims), and a domain belongs to the organization whose own verification record resolved last, so publishing that record takes a name another organization holds. The dnsRecords array specifies what 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. DNS is re-checked on a schedule and nothing pushes an event when it is, so status refreshes are pull-only.
GET /v1/domains
Section titled “GET /v1/domains”List sending domains
Auth: 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 } ], "challengeToken": "string", "challengeVerifiedAt": "2026-01-15T09:30:00.000Z", "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, "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[].domain |
string | yes | |
data[].region |
string | yes | |
data[].status |
“not_started” | “pending” | “success” | “failed” | “temporary_failure” | “blocked” | 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” | “blocked” | yes | |
data[].dnsRecords[].recommended |
boolean | no | |
data[].challengeToken |
string | yes | |
data[].challengeVerifiedAt |
string (date-time) | null | yes | |
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 | |
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/domains/{id}
Section titled “GET /v1/domains/{id}”Fetch one sending domain
Auth: 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 } ], "challengeToken": "string", "challengeVerifiedAt": "2026-01-15T09:30:00.000Z", "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, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| 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” | “blocked” | 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” | “blocked” | yes | |
data.dnsRecords[].recommended |
boolean | no | |
data.challengeToken |
string | yes | |
data.challengeVerifiedAt |
string (date-time) | null | yes | |
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 | |
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/domains
Section titled “POST /v1/domains”Add a sending domain
Auth: 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 } ], "challengeToken": "string", "challengeVerifiedAt": "2026-01-15T09:30:00.000Z", "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, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| 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” | “blocked” | 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” | “blocked” | yes | |
data.dnsRecords[].recommended |
boolean | no | |
data.challengeToken |
string | yes | |
data.challengeVerifiedAt |
string (date-time) | null | yes | |
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 | |
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/domains/{id}/dns-setup
Section titled “GET /v1/domains/{id}/dns-setup”Where the DNS provider can publish the records
The Domain Connect link for this domain’s DNS provider, which shows the admin the records and asks them to confirm. url is null when the provider does not support it.
Auth: Session token.
Response 200
{ "data": { "url": "https://example.com/hooks/cowliss", "provider": "string" }, "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) | null | yes | |
data.provider |
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/domains/{id}/verify
Section titled “POST /v1/domains/{id}/verify”Re-check DNS now
Auth: 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 } ], "challengeToken": "string", "challengeVerifiedAt": "2026-01-15T09:30:00.000Z", "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, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| 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” | “blocked” | 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” | “blocked” | yes | |
data.dnsRecords[].recommended |
boolean | no | |
data.challengeToken |
string | yes | |
data.challengeVerifiedAt |
string (date-time) | null | yes | |
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 | |
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/domains/{id}
Section titled “PATCH /v1/domains/{id}”Toggle click tracking
Enabling click tracking requires a verified domain, while disabling is always allowed. Tracking covers every email sent from the domain, including transactional email.
Auth: 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 } ], "challengeToken": "string", "challengeVerifiedAt": "2026-01-15T09:30:00.000Z", "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, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| 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” | “blocked” | 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” | “blocked” | yes | |
data.dnsRecords[].recommended |
boolean | no | |
data.challengeToken |
string | yes | |
data.challengeVerifiedAt |
string (date-time) | null | yes | |
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 | |
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.
DELETE /v1/domains/{id}
Section titled “DELETE /v1/domains/{id}”Give up a sending domain
Sending from this domain stops immediately, and the domain becomes available for any organization to claim.
Auth: Session token.
Response 200
{ "data": { "deleted": true }, "meta": { "requestId": "string", "nextCursor": "string", "hasMore": true, "warnings": [ { "code": "string", "message": "string", "suggestion": "string" } ] }}| 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 | |
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.