# Sending domains

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

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

List sending domains

**Auth:** Session token.

**Query parameters**

```json
{
  "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**

```json
{
  "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](/api-conventions/#errors).

## GET /v1/domains/{id}

Fetch one sending domain

**Auth:** Session token.

**Response 200**

```json
{
  "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](/api-conventions/#errors).

## POST /v1/domains

Add a sending domain

**Auth:** Session token.

**Request body**

```json
{
  "data": {
    "domain": "string"
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.domain` | string | yes | length 0–253 |

**Response 201**

```json
{
  "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](/api-conventions/#errors).

## 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**

```json
{
  "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](/api-conventions/#errors).

## POST /v1/domains/{id}/verify

Re-check DNS now

**Auth:** Session token.

**Response 200**

```json
{
  "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](/api-conventions/#errors).

## 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**

```json
{
  "data": {
    "clickTracking": true
  }
}
```

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | object | yes |  |
| `data.clickTracking` | boolean | yes |  |

**Response 200**

```json
{
  "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](/api-conventions/#errors).

## 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**

```json
{
  "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](/api-conventions/#errors).
