# Domains

A domain is the identity your mail is signed with. Add it, publish its DNS records, verify it, and it can send.

## The records

`POST /domains` answers with a `records` array. Publish the entries for what you use, exactly as given, at your DNS provider. Names are relative to your DNS zone: the table shows them for a domain at the root of its zone, and for `send.acme.example` each one carries `.send` — `rasket._domainkey.send`, `send.send`, `links.send`.

| Record | Type | Name | Value |
| --- | --- | --- | --- |
| DKIM | `TXT` | `rasket._domainkey` | A bare `p=…` value. The key is 2048-bit, so the value is published as two quoted strings — most providers do that for you when you paste it. |
| MAIL FROM | `CNAME` | `send` | `<code>.rasket.net` for the domain's region — `us1.rasket.net` for `us-east-1`. This is what makes bounces come back to us instead of to your own mail server, and what your SPF check passes on. Publish it DNS only, never proxied. Some domains are given an `MX` and a `TXT` at `send` instead; publish whichever `records` returns — both work. |
| Receiving | `MX` | `@` | Our inbound mail host for your region, priority 10, at `receiving_host` (`@` by default). Only in `us-east-1` and `eu-west-1`, and only needed once you turn receiving on. |
| Tracking | `CNAME` | `links` | `links1.rasket-dns.com`. Only needed if you turn on open or click tracking, and never proxied. |

The first two are what sending needs. The `Receiving` and `Tracking` rows are in the array from the start, whatever the toggles say, but only have to verify once you turn receiving, or open or click tracking, on — a domain verifies for sending without them. The [tracking guide](https://www.rasket.com/docs/tracking) covers it. No CAA record is required for any of them.

> Send from a subdomain — `send.acme.example` rather than `acme.example`. A subdomain keeps your transactional reputation separate from whatever else your root domain does, and it means these records never collide with the ones your mail provider already publishes.

## Publishing them for you

`POST /domains/{domain_id}/autoconfigure` writes the records through your DNS provider. Cloudflare is the only provider we can configure today. It takes an API token created from the `Zone → DNS → Edit` template and scoped to this domain's zone; a token covering more than one zone is refused, and a global API key is refused outright.

- Only the records for capabilities you have enabled are published. Turn tracking on first if you want its record written too.
- We never delete a record and never overwrite one we did not create. Anything already sitting at one of our names comes back in `conflicts` for you to resolve.
- The token is deleted as soon as the records are written unless you pass `keep_token`. `DELETE /domains/{domain_id}/autoconfigure/credential` removes a kept one — that stops us using it, not anyone else, so revoke it at Cloudflare too.

## Regions

`region` selects where your mail is signed and relayed from. It is a deliverability and latency choice, not a data residency one: it does not decide where your data is stored. A domain's region is fixed once it is created.

## Verification

- `POST /domains/{domain_id}/verify` checks now and returns the domain with each record's status. A second call within 15 seconds returns the current state without checking again.
- We also re-check every domain on a schedule, so a record you publish will verify on its own without you polling.
- A record that verified and later stops resolving is reported as drifted rather than silently ignored.
- A record's `status` is about DNS only. A Receiving MX can read `verified` while `capabilities.receiving` is `disabled`: the record is in place, and receiving is off until you turn it on.

## Claiming a domain someone else verified

A domain can be verified on one team at a time, so adding one another team already holds answers `409 resource_locked`. `POST /domains/claim` is how you take it over, and the only way: prove you control the name in DNS.

The claim comes back with a TXT record to publish at the root of the domain. Its value always starts `rasket-domain-verification=`:

The ownership record a claim asks for:

```text
TXT  @  rasket-domain-verification=8Kx2mQ7vN4pR9wL1sT6yU3bC5dF0gH8jK2nM4qP7rS9
```

- When the record resolves, the domain moves to your team and the previous owner's copy is switched off. There is no other route — no support ticket and no override.
- The transfer waits if the current owner verified the domain in the last 72 hours, sent from it in the last 24 hours, or has mail scheduled from it. `blocked_reason` says which, and we re-check every hour.
- A claim expires after 7 days. The domain arrives with fresh DKIM records of its own — the previous owner's values are never reusable — so publish those and verify as normal.

## TLS

`tls` is `opportunistic` by default: we use TLS to the receiving server when it offers it. Setting it to `enforced` changes what happens when it does not — messages to mail servers that do not support TLS will not be sent and will appear as `email.failed`. That is the trade: no plaintext delivery, at the cost of some mail not arriving.

## DMARC

DMARC is not required to verify a domain and we do not publish one for you. It is worth adding, starting at `p=none` and tightening once the reports look right:

A starting DMARC record:

```text
TXT  _dmarc  v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com
```

> If your domain already publishes `_dmarc` with `p=reject` and the MAIL FROM records above are not yet verified, mail you send through us may be rejected outright until alignment is in place. Publish the records first, verify, then send.

## Endpoints

### `POST /domains`

Register a sending domain and get the DNS records to publish.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` (required) | string | The domain to send from. A subdomain such as `send.acme.example` is recommended. |
| `region` | string | `us-east-1`, `eu-west-1`, `sa-east-1` or `ap-northeast-1`. Defaults to `us-east-1`. Receiving is available in `us-east-1` and `eu-west-1` only. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `open_tracking` | boolean | Rewrite the body to add a tracking pixel. Off by default. |
| `click_tracking` | boolean | Rewrite links so clicks are recorded. Off by default. |
| `tracking_subdomain` | string | The label tracking links are served from. Defaults to `links`. |
| `custom_return_path` | string | The label the sending records are published at. Leave it out and we pick the first label no other email service uses at your domain: `rasket`, then `rasketmail`, `rk`, `rasket1`, `rasket2`. A label that already holds another service's records answers `422 invalid_parameter` with `details.reason` `return_path_in_use` and a free `details.suggestion`. |
| `receiving_host` | string | Where the domain receives mail: `@` by default (`anything@<name>`), or a label such as `inbound` for `anything@inbound.<name>`. An MX at `@` takes the domain's mail from its current mailbox provider. |
| `confirm_replace_mx` | boolean | Only with receiving enabled. If the receiving name's MX already delivers to another mailbox provider, the request is refused with `409 receiving_mx_in_use` unless this is `true`. Use a `receiving_host` label instead to leave that mail where it is. |
| `tls` | string | `opportunistic` (the default) or `enforced`. Enforced refuses to deliver without TLS. |
| `capabilities` | object | `{ sending, receiving }`, each `enabled` or `disabled`. Defaults to sending on, receiving off; at least one must be on. Receiving in a region without it is `400 validation_error`. |

Add a domain:

```sh
curl -X POST "https://api.rasket.com/domains" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "send.acme.example",
  "region": "eu-west-1"
}'
```

```ts
const response = await fetch("https://api.rasket.com/domains", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "send.acme.example",
    region: "eu-west-1"
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/domains",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "name": "send.acme.example",
    "region": "eu-west-1"
  },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "name": "send.acme.example",
  "created_at": "2026-09-09T09:02:44.301Z",
  "status": "not_started",
  "region": "eu-west-1",
  "open_tracking": false,
  "click_tracking": false,
  "tracking_subdomain": "links",
  "receiving_host": "@",
  "custom_return_path": "rasket",
  "capabilities": {
    "sending": "enabled",
    "receiving": "disabled"
  },
  "managed": false,
  "records": [
    {
      "record": "DKIM",
      "name": "rasket._domainkey.send",
      "type": "TXT",
      "ttl": "Auto",
      "status": "not_started",
      "value": "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA…"
    },
    {
      "record": "SPF",
      "name": "rasket.send",
      "type": "CNAME",
      "ttl": "Auto",
      "status": "not_started",
      "value": "eu1.rasket.net"
    },
    {
      "record": "Tracking",
      "name": "links.send",
      "type": "CNAME",
      "ttl": "Auto",
      "status": "not_started",
      "value": "links1.rasket-dns.com"
    }
  ]
}
```

- Publish the records for each capability you turned on (DKIM and the SPF CNAME for sending), then call verify. Nothing sends until the domain is verified.
- `records` lists every group whatever the toggles say — Tracking, and in `us-east-1` and `eu-west-1` a Receiving MX at `receiving_host` — so you can see what turning one on would need. Record names are relative to your DNS zone: for `send.acme.example` in `acme.example`, the return-path CNAME sits at `rasket.send`.
- `region` is where your mail is sent from, not where your data is stored.
- A name another team has already verified answers `409 resource_locked`.
- `custom_return_path` in the response is the label we chose when you named none.

### `GET /domains`

Every domain on the team.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |
| `search` | string | Text in the domain name. Case-insensitive, 1–200 characters. |
| `status` | string | Only domains in these states, comma-separated: `not_started`, `pending`, `verified`, `partially_verified`, `partially_failed`, `failed`. |

List domains:

```sh
curl -X GET "https://api.rasket.com/domains" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/domains", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/domains",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
      "name": "send.acme.example",
      "status": "verified",
      "created_at": "2026-09-09T09:02:44.301Z",
      "region": "eu-west-1",
      "open_tracking": false,
      "click_tracking": false,
      "capabilities": {
        "sending": "enabled",
        "receiving": "disabled"
      },
      "managed": false
    }
  ]
}
```

- The list omits `records`. Retrieve one domain to see the current state of each record.

### `GET /domains/{domain_id}`

One domain, with the live status of every record.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The domain's ID. |

Retrieve a domain:

```sh
curl -X GET "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "domain",
  "id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "name": "send.acme.example",
  "status": "verified",
  "created_at": "2026-09-09T09:02:44.301Z",
  "region": "eu-west-1",
  "open_tracking": false,
  "click_tracking": false,
  "tracking_subdomain": "links",
  "receiving_host": "@",
  "custom_return_path": "rasket",
  "capabilities": {
    "sending": "enabled",
    "receiving": "disabled"
  },
  "managed": false,
  "records": [
    {
      "record": "DKIM",
      "name": "rasket._domainkey.send",
      "type": "TXT",
      "ttl": "Auto",
      "status": "not_started",
      "value": "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA…"
    },
    {
      "record": "SPF",
      "name": "rasket.send",
      "type": "CNAME",
      "ttl": "Auto",
      "status": "not_started",
      "value": "eu1.rasket.net"
    },
    {
      "record": "Tracking",
      "name": "links.send",
      "type": "CNAME",
      "ttl": "Auto",
      "status": "not_started",
      "value": "links1.rasket-dns.com"
    }
  ]
}
```

### `PATCH /domains/{domain_id}`

Change tracking, TLS, the tracking subdomain, the return path or where the domain receives mail.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The domain's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `open_tracking` | boolean | Turn the tracking pixel on or off. |
| `click_tracking` | boolean | Turn link rewriting on or off. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `tls` | string | `opportunistic` or `enforced`. |
| `tracking_subdomain` | string | Changing this publishes a new record and re-verifies tracking. |
| `receiving_host` | string | A label or `@`. Changing it moves the Receiving MX to the new name and re-verifies receiving; mail is accepted at the new address once that MX verifies. |
| `custom_return_path` | string | Changing it moves the sending records to the new label: publish them there and keep the old ones until the new ones verify. The domain keeps sending meanwhile. A label that already holds another service's records answers `422 invalid_parameter`. |
| `confirm_replace_mx` | boolean | Turning receiving on, or moving `receiving_host`, at a name whose MX already delivers to another mailbox provider is `409 receiving_mx_in_use` unless this is `true`. |
| `capabilities` | object | `{ sending, receiving }`, each `enabled` or `disabled`; name one or both. At least one must stay on. |
| `region` | string | Fixed at creation. Sending it is `422 invalid_parameter`. |

Update a domain:

```sh
curl -X PATCH "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "click_tracking": true
}'
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    click_tracking: true
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "click_tracking": True
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "domain",
  "id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34"
}
```

- The domain's name and region are fixed at creation. Delete and re-add to change either.

### `POST /domains/{domain_id}/verify`

Check the published records now and get the domain back with each record's status.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The domain's ID. |

Verify a domain:

```sh
curl -X POST "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/verify" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/verify", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/verify",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "domain",
  "id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "name": "send.acme.example",
  "status": "verified",
  "created_at": "2026-09-09T09:02:44.301Z",
  "region": "eu-west-1",
  "open_tracking": false,
  "click_tracking": false,
  "tracking_subdomain": "links",
  "receiving_host": "@",
  "custom_return_path": "rasket",
  "capabilities": {
    "sending": "enabled",
    "receiving": "disabled"
  },
  "managed": false,
  "records": [
    {
      "record": "DKIM",
      "name": "rasket._domainkey.send",
      "type": "TXT",
      "ttl": "Auto",
      "status": "not_started",
      "value": "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA…"
    },
    {
      "record": "SPF",
      "name": "rasket.send",
      "type": "CNAME",
      "ttl": "Auto",
      "status": "not_started",
      "value": "eu1.rasket.net"
    },
    {
      "record": "Tracking",
      "name": "links.send",
      "type": "CNAME",
      "ttl": "Auto",
      "status": "not_started",
      "value": "links1.rasket-dns.com"
    }
  ]
}
```

- The check runs immediately and the call waits up to 10 seconds for it.
- A second call within 15 seconds returns the current state without checking again. So does a call while a check is already running, or one whose check does not finish in time; the scheduled check then finishes it.
- We also re-check every domain on a schedule, so a published record verifies on its own within the hour.
- Verification reads DNS. A record published seconds ago may still be cached as absent by the resolver.

### `DELETE /domains/{domain_id}`

Delete a domain.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The domain's ID. |

Delete a domain:

```sh
curl -X DELETE "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.delete(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "domain",
  "id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "deleted": true
}
```

- Emails already sent from the domain are kept; scheduled ones that have not gone out will fail.
- Any `sending_access` key restricted to this domain stops being able to send.

### `POST /domains/{domain_id}/dkim/regenerate`

Mint a new signing key, optionally a shorter one, and republish the DKIM record.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The domain's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `key_bits` (required) | integer | `2048` to rotate at the default size, or `1024` for a DNS panel that will not accept the ~392-character TXT value a 2048-bit key produces. |

Regenerate the DKIM key:

```sh
curl -X POST "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/dkim/regenerate" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "key_bits": 1024
}'
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/dkim/regenerate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    key_bits: 1024
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/dkim/regenerate",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "key_bits": 1024
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "domain",
  "id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "name": "send.acme.example",
  "status": "verified",
  "created_at": "2026-09-09T09:02:44.301Z",
  "region": "eu-west-1",
  "open_tracking": false,
  "click_tracking": false,
  "tracking_subdomain": "links",
  "receiving_host": "@",
  "custom_return_path": "rasket",
  "capabilities": {
    "sending": "enabled",
    "receiving": "disabled"
  },
  "managed": false,
  "records": [
    {
      "record": "DKIM",
      "name": "rasket._domainkey.send",
      "type": "TXT",
      "ttl": "Auto",
      "status": "not_started",
      "value": "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA…"
    },
    {
      "record": "SPF",
      "name": "rasket.send",
      "type": "CNAME",
      "ttl": "Auto",
      "status": "not_started",
      "value": "eu1.rasket.net"
    },
    {
      "record": "Tracking",
      "name": "links.send",
      "type": "CNAME",
      "ttl": "Auto",
      "status": "not_started",
      "value": "links1.rasket-dns.com"
    }
  ]
}
```

- Publish the DKIM record's new value before your next send. The old key stops signing immediately, and the domain is not verified for sending again until we see the new record.
- One call per minute per domain. Verifying the domain within that minute also counts.

### `POST /domains/claim`

Take over a domain another team has already verified, by proving you control it.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` (required) | string | The domain to claim. Adding it with `POST /domains` answered `409` for this name. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `region` | string | Where your mail will be sent from once the claim completes. It does not have to match the current owner's. |
| `custom_return_path` | string | The return-path label, as on `POST /domains`. Leave it out and we pick a free one. |
| `tracking_subdomain` | string | As on `POST /domains`; applies to the domain once the claim completes. |
| `receiving_host` | string | As on `POST /domains`; applies to the domain once the claim completes. |
| `open_tracking` | boolean | As on `POST /domains`; applies to the domain once the claim completes. |
| `click_tracking` | boolean | As on `POST /domains`; applies to the domain once the claim completes. |
| `tls` | string | As on `POST /domains`; applies to the domain once the claim completes. |
| `capabilities` | object | As on `POST /domains`; applies to the domain once the claim completes. |

Claim a domain:

```sh
curl -X POST "https://api.rasket.com/domains/claim" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "example.com",
  "region": "us-east-1"
}'
```

```ts
const response = await fetch("https://api.rasket.com/domains/claim", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "example.com",
    region: "us-east-1"
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/domains/claim",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "name": "example.com",
    "region": "us-east-1"
  },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "object": "domain_claim",
  "id": "c1f2b3a4-1176-453e-8fc1-35364d380206",
  "name": "example.com",
  "status": "pending",
  "domain_id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "region": "us-east-1",
  "record": {
    "record": "Claim",
    "name": "example.com",
    "type": "TXT",
    "ttl": "Auto",
    "status": "pending",
    "value": "rasket-domain-verification=8Kx2mQ7vN4pR9wL1sT6yU3bC5dF0gH8jK2nM4qP7rS9"
  },
  "blocked_reason": null,
  "failure_reason": null,
  "created_at": "2026-09-09T00:00:00.000Z",
  "expires_at": "2026-09-16T00:00:00.000Z"
}
```

- Publish the returned TXT record at the root of the domain. We check it on a schedule; when it resolves, the domain moves to your team and the previous owner's copy is switched off.
- The transfer waits if the current owner verified the domain in the last 72 hours, sent from it in the last 24 hours, or has scheduled mail from it. `blocked_reason` says which, and we re-check every hour.
- A claim expires after 7 days. Calling this again while a claim is open returns that claim unchanged with `200` rather than starting a second one.
- The domain arrives with new DKIM records of its own: the previous owner's values are never reusable. Publish them and verify the domain as normal.

### `GET /domains/{domain_id}/claim`

The latest claim for a domain, and the record that proves ownership.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The placeholder domain the claim created — not the claim's own ID. |

Retrieve a domain claim:

```sh
curl -X GET "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/claim" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/claim", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/claim",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "domain_claim",
  "id": "c1f2b3a4-1176-453e-8fc1-35364d380206",
  "name": "example.com",
  "status": "pending",
  "domain_id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "region": "us-east-1",
  "record": {
    "record": "Claim",
    "name": "example.com",
    "type": "TXT",
    "ttl": "Auto",
    "status": "pending",
    "value": "rasket-domain-verification=8Kx2mQ7vN4pR9wL1sT6yU3bC5dF0gH8jK2nM4qP7rS9"
  },
  "blocked_reason": null,
  "failure_reason": null,
  "created_at": "2026-09-09T00:00:00.000Z",
  "expires_at": "2026-09-16T00:00:00.000Z"
}
```

- Poll this to follow a claim: `pending` while we look for the record, `blocked` with a reason while the current owner is still active, `completed` once the domain is yours.

### `POST /domains/{domain_id}/claim/verify`

Ask for the ownership record to be checked sooner.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The placeholder domain the claim created — not the claim's own ID. |

Verify a domain claim:

```sh
curl -X POST "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/claim/verify" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/claim/verify", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/claim/verify",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "domain_claim",
  "id": "c1f2b3a4-1176-453e-8fc1-35364d380206",
  "name": "example.com",
  "status": "pending",
  "domain_id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "region": "us-east-1",
  "record": {
    "record": "Claim",
    "name": "example.com",
    "type": "TXT",
    "ttl": "Auto",
    "status": "pending",
    "value": "rasket-domain-verification=8Kx2mQ7vN4pR9wL1sT6yU3bC5dF0gH8jK2nM4qP7rS9"
  },
  "blocked_reason": null,
  "failure_reason": null,
  "created_at": "2026-09-09T00:00:00.000Z",
  "expires_at": "2026-09-16T00:00:00.000Z"
}
```

- The claim stays `pending` while the check runs; read the claim back to see the result.
- One call per minute per domain. Verifying the domain within that minute also counts.

### `POST /domains/{domain_id}/autoconfigure`

Publish this domain's records through the customer's DNS provider.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The domain's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `provider` (required) | string | `cloudflare`. The only provider we can configure today. |
| `token` (required) | string | A Cloudflare API token created from the `Zone → DNS → Edit` template, scoped to this domain's zone and no other. Global API keys are refused. |
| `keep_token` | boolean | `true` to keep the token so you can re-apply records later. It is deleted as soon as the records are written otherwise. |

Auto configure DNS:

```sh
curl -X POST "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/autoconfigure" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "provider": "cloudflare",
  "token": "your-cloudflare-api-token",
  "keep_token": false
}'
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/autoconfigure", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    provider: "cloudflare",
    token: "your-cloudflare-api-token",
    keep_token: false
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/autoconfigure",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "provider": "cloudflare",
    "token": "your-cloudflare-api-token",
    "keep_token": False
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "domain",
  "id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "created": ["DKIM:TXT:rasket._domainkey.send", "SPF:CNAME:rasket.send"],
  "updated": [],
  "conflicts": [],
  "credential_kept": false
}
```

- Only records for capabilities you have enabled are published. Turning on receiving or tracking first, then running this, is what publishes those records.
- We never delete a record, and never overwrite one we did not create. Anything already sitting at one of our names comes back in `conflicts` for you to resolve.
- The token is encrypted at rest and only ever used for this domain's zone. Revoke it at Cloudflare when you are done — deleting it here stops us using it, not anyone else.
- One call per minute per domain. Verifying the domain within that minute also counts.

### `DELETE /domains/{domain_id}/autoconfigure/credential`

Remove the provider credential kept for this domain.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The domain's ID. |

Delete the stored DNS token:

```sh
curl -X DELETE "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/autoconfigure/credential" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/autoconfigure/credential", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.delete(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/autoconfigure/credential",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "domain",
  "id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "deleted": true
}
```

- Records already published are untouched; only our copy of the token goes away.
- Answers 404 when no token is stored for this domain.

### `GET /domains/{domain_id}/records`

Each record to publish, beside what DNS answered for it on the last check.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The domain's ID. |

Compare a domain's records with DNS:

```sh
curl -X GET "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/records" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/records", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/records",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "domain_records",
  "domain_id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "sending_verified": false,
  "tracking_verified": false,
  "receiving_verified": false,
  "receiving_host": "@",
  "receiving_address": null,
  "records": [
    {
      "record": "DKIM",
      "name": "rasket._domainkey.send",
      "type": "TXT",
      "ttl": "Auto",
      "value": "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA…",
      "status": "failed",
      "observed": [],
      "reason": "not_found",
      "last_checked_at": "2026-09-09T09:30:00.000Z"
    }
  ]
}
```

- A record with nothing in `observed` was not found at its `name`. One whose `observed` value differs from `value` was published wrong.
- This reads the last check. Ask for a new one with `POST /domains/{domain_id}/verify`.

### `POST /domains/{domain_id}/logo`

Store an SVG as this domain's brand logo and get the URL it is served at.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The domain's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `svg` (required) | string | The logo, as SVG source. It has to be a square SVG Tiny Portable/Secure file: a `<title>`, `baseProfile="tiny-ps"`, `version="1.2"`, a square `viewBox`, and none of `<script>`, `<image>`, `<use>`, `<a>`, animation, event handlers or references to anything outside the file. |

Upload the brand logo:

```sh
curl -X POST "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" baseProfile=\"tiny-ps\" version=\"1.2\" viewBox=\"0 0 64 64\"><title>Acme</title><rect width=\"64\" height=\"64\" fill=\"#1f6feb\"/></svg>"
}'
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    svg: "<svg xmlns=\"http://www.w3.org/2000/svg\" baseProfile=\"tiny-ps\" version=\"1.2\" viewBox=\"0 0 64 64\"><title>Acme</title><rect width=\"64\" height=\"64\" fill=\"#1f6feb\"/></svg>"
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" baseProfile=\"tiny-ps\" version=\"1.2\" viewBox=\"0 0 64 64\"><title>Acme</title><rect width=\"64\" height=\"64\" fill=\"#1f6feb\"/></svg>"
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "domain_brand_logo",
  "domain_id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "url": "https://share.example.com/bimi/d91b8a2e-2f3d-4f2a-9a0b-3c1e5f7a9b21/9d3c5d18b976973c1b6e4bcc0d7d3dfc76db4a69ccdb9aaebbc78e919d623909.svg",
  "bytes": 166,
  "sha256": "9d3c5d18b976973c1b6e4bcc0d7d3dfc76db4a69ccdb9aaebbc78e919d623909",
  "uploaded_at": "2026-09-09T09:30:00.000Z"
}
```

- Nothing is repaired or stripped. A file that breaks any of the rules is refused with one sentence per rule it breaks, and no logo is stored.
- The URL carries the digest of the bytes it serves, so uploading a new logo gives you a new URL. Publish the `default._bimi` record again after you do.
- The logo is at most 32 KB, and one per domain — uploading again replaces the one that is there.
- Whether a recipient sees it is up to their mail app. Most show a self-asserted logo only once your DMARC policy is at `quarantine` or `reject`, and some show one only with a paid certificate.

### `DELETE /domains/{domain_id}/logo`

Delete this domain's brand logo and the URL it was served at.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The domain's ID. |

Remove the brand logo:

```sh
curl -X DELETE "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.delete(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "domain_brand_logo",
  "domain_id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "deleted": true
}
```

- A domain with no logo answers the same way, so this is safe to call twice.
- The URL stops answering straight away. A `default._bimi` record still published points at nothing until it is generated again.

### `GET /domains/{domain_id}/logo/certificate`

See where this domain's brand certificate has got to.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The domain's ID. |

Read the brand certificate:

```sh
curl -X GET "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo/certificate" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo/certificate", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo/certificate",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "domain_brand_certificate",
  "domain_id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "state": "active",
  "csr_pem": "-----BEGIN CERTIFICATE REQUEST-----\nMIICrTCC…\n-----END CERTIFICATE REQUEST-----\n",
  "subject": {
    "common_name": "mail.example.com",
    "organization": "Acme Corporation",
    "organizational_unit": null,
    "locality": "Portland",
    "state": "Oregon",
    "country": "US"
  },
  "requested_at": "2026-09-09T09:30:00.000Z",
  "url": "https://share.example.com/bimi/d91b8a2e-2f3d-4f2a-9a0b-3c1e5f7a9b21/5f2a9b21d91b8a2e2f3d4f2a9a0b3c1e5f7a9b21d91b8a2e2f3d4f2a9a0b3c1e.pem",
  "bytes": 6144,
  "sha256": "5f2a9b21d91b8a2e2f3d4f2a9a0b3c1e5f7a9b21d91b8a2e2f3d4f2a9a0b3c1e",
  "issuer": "DigiCert Inc",
  "expires_at": "2027-09-09T09:30:00.000Z",
  "installed_at": "2026-09-16T11:02:00.000Z",
  "published": true,
  "reason": null
}
```

- `state` is `none`, `awaiting_certificate`, `active`, `logo_changed` or `expired`. Only `active` is published.
- `logo_changed` means the logo was replaced after the certificate was issued. Nothing is thrown away: put the previous logo back, or ask your authority to re-issue for the new one.
- The private key never leaves us and is in no response.

### `POST /domains/{domain_id}/logo/certificate`

Generate the request to take to a certificate authority.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The domain's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `common_name` (required) | string | The domain the logo is published for, such as `mail.example.com`. |
| `organization` (required) | string | Your organisation's legal name, exactly as registered. The authority checks it against your documents. |
| `organizational_unit` | string | An optional division within the organisation. |
| `locality` (required) | string | City or town. |
| `state` (required) | string | State, province or region. |
| `country` (required) | string | Two-letter country code, such as `US`. |

Create a certificate signing request:

```sh
curl -X POST "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo/certificate" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "common_name": "mail.example.com",
  "organization": "Acme Corporation",
  "locality": "Portland",
  "state": "Oregon",
  "country": "US"
}'
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo/certificate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    common_name: "mail.example.com",
    organization: "Acme Corporation",
    locality: "Portland",
    state: "Oregon",
    country: "US"
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo/certificate",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "common_name": "mail.example.com",
    "organization": "Acme Corporation",
    "locality": "Portland",
    "state": "Oregon",
    "country": "US"
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "domain_brand_certificate",
  "domain_id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "state": "awaiting_certificate",
  "csr_pem": "-----BEGIN CERTIFICATE REQUEST-----\nMIICrTCC…\n-----END CERTIFICATE REQUEST-----\n",
  "subject": {
    "common_name": "mail.example.com",
    "organization": "Acme Corporation",
    "organizational_unit": null,
    "locality": "Portland",
    "state": "Oregon",
    "country": "US"
  },
  "requested_at": "2026-09-09T09:30:00.000Z",
  "url": null,
  "bytes": null,
  "sha256": null,
  "issuer": null,
  "expires_at": null,
  "installed_at": null,
  "published": false,
  "reason": "Take the request below to a certificate authority. When they issue your certificate, paste it here."
}
```

- This needs a paid plan, and the domain needs a brand logo: a certificate is issued for the logo it will be published with.
- You buy the certificate from the authority. We charge nothing for it and never see the transaction.
- Calling this again while a request is open returns the same request, because that one may already be with the authority. Delete the certificate first to start over.

### `PATCH /domains/{domain_id}/logo/certificate`

Store the chain the authority issued and publish it as the record's `a=` tag.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The domain's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `pem` (required) | string | The certificate chain, PEM-encoded: your certificate first, then each one that issued the one above it. Never a private key. |

Install the issued certificate:

```sh
curl -X PATCH "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo/certificate" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "pem": "-----BEGIN CERTIFICATE-----\nMIIFyzCC…\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEwTCC…\n-----END CERTIFICATE-----\n"
}'
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo/certificate", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    pem: "-----BEGIN CERTIFICATE-----\nMIIFyzCC…\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEwTCC…\n-----END CERTIFICATE-----\n"
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo/certificate",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "pem": "-----BEGIN CERTIFICATE-----\nMIIFyzCC…\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEwTCC…\n-----END CERTIFICATE-----\n"
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "domain_brand_certificate",
  "domain_id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "state": "active",
  "csr_pem": "-----BEGIN CERTIFICATE REQUEST-----\nMIICrTCC…\n-----END CERTIFICATE REQUEST-----\n",
  "subject": {
    "common_name": "mail.example.com",
    "organization": "Acme Corporation",
    "organizational_unit": null,
    "locality": "Portland",
    "state": "Oregon",
    "country": "US"
  },
  "requested_at": "2026-09-09T09:30:00.000Z",
  "url": "https://share.example.com/bimi/d91b8a2e-2f3d-4f2a-9a0b-3c1e5f7a9b21/5f2a9b21d91b8a2e2f3d4f2a9a0b3c1e5f7a9b21d91b8a2e2f3d4f2a9a0b3c1e.pem",
  "bytes": 6144,
  "sha256": "5f2a9b21d91b8a2e2f3d4f2a9a0b3c1e5f7a9b21d91b8a2e2f3d4f2a9a0b3c1e",
  "issuer": "DigiCert Inc",
  "expires_at": "2027-09-09T09:30:00.000Z",
  "installed_at": "2026-09-16T11:02:00.000Z",
  "published": true,
  "reason": null
}
```

- The chain is accepted only if its first certificate carries the public key of this domain's request, it is inside its validity window, and each certificate is signed by the one after it.
- Publish the `default._bimi` record again afterwards: its value has changed.

### `DELETE /domains/{domain_id}/logo/certificate`

Delete the request and the certificate, and go back to the self-asserted record.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `domain_id` (required) | string | The domain's ID. |

Remove the brand certificate:

```sh
curl -X DELETE "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo/certificate" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo/certificate", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.delete(
    "https://api.rasket.com/domains/d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34/logo/certificate",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "domain_brand_certificate",
  "domain_id": "d91a7b60-1a5f-4a2e-9d1b-0d9f2c7a1e34",
  "deleted": true
}
```

- The logo is untouched. `default._bimi` goes back to the value it had before the certificate.
- A domain with no certificate answers the same way, so this is safe to call twice.
