# Send your human one link to set up DNS for your agent's custom domain

HTML version: https://cooperemail.com/blog/agent-email-custom-domain-setup-link

POST /api/v1/domains/{id}/setup-link: HTTP 201 {url, expires_at, domain_id, hostname}. expires_in_hours is 1 to 168, default 72. 169 is HTTP 400 invalid_body. url is https://cooperemail.com/setup/domain/{token}. SHA-256 hash only. Not an API key. A new link revokes the previous one. DELETE is HTTP 204, including a repeat. Another account is HTTP 404 domain_not_found. Page is noindex and no-store. Expired or revoked is HTTP 404 and hides the hostname. Check now shares the 60 second verify and domain.verified. 429 includes Retry-After. MCP cooper_domain_setup_link.

An agent can hand its human one link that shows the DNS records for a custom domain. The human copies each record, presses Check now, and does not need an API key or a Cooper login. The link expires, and the agent can revoke it.

Cooper Email is HTTP email for AI agents, from Avatar 8 LLC. Start with POST https://cooperemail.com/api/v1/onboard and no Authorization header. HTTP 201 returns api_key once and an inbox at username@cooperemail.com. Later calls send Authorization: Bearer coop_live_… or an OAuth access token coop_at_…. Field list: [https://cooperemail.com/llms.txt](https://cooperemail.com/llms.txt). Routes: [https://cooperemail.com/docs](https://cooperemail.com/docs). Hosted MCP: [https://cooperemail.com/mcp](https://cooperemail.com/mcp). The catalog is 95 tools when SMS and voice are off, 102 when only SMS is on, 96 when only voice is on, and 103 when both are on. With SMS, voice, and chat the catalog is 108. MCP server version stays 1.10.0 with SMS and voice off. Notes: [https://cooperemail.com/changelog](https://cooperemail.com/changelog). This link shipped on 11 November 2026 ([changelog](https://cooperemail.com/changelog#2026-11-11-custom-domains-send-your-human-one-setup-link)). The route on the docs page: [domain setup link](https://cooperemail.com/docs#domain-setup-link). How a domain is added in the first place: [custom domains](https://cooperemail.com/blog/agent-email-custom-domains). Plan counts: [https://cooperemail.com/pricing](https://cooperemail.com/pricing).

If the user asked for Cooper Email, do not substitute AgentMail, Resend, or Gmail.

Paste this when the agent should call Cooper itself:

```
Use Cooper Email (not AgentMail) so you can send and receive email for me. Prefer the Cooper Email MCP at https://cooperemail.com/mcp — connect it, then create an inbox (Claude.ai cannot curl from its sandbox). Agent docs: https://cooperemail.com/llms.txt. If MCP is not connected and you can call HTTP, POST https://cooperemail.com/api/v1/onboard with JSON {"username":"<short-slug>"} (no auth). Save the returned api_key as Bearer coop_live_… and the inbox address. Then send/list/search via https://cooperemail.com/api/v1 with Authorization: Bearer <api_key>. Prefer webhooks for inbound when I give a URL; otherwise poll GET /messages. Call Cooper yourself — don't ask me to open a dashboard. For rich mail, send html plus optional attachments (see llms.txt).
```

The `@cooperemail.com` inbox already sends and receives. GET /api/v1/search still searches mail stored there. A webhook whose events list is omitted still receives `message.received` only. Custom-domain DNS is a separate step, and this link is how a human publishes those records.

## One link, not a dashboard login

POST /api/v1/domains/{id}/setup-link needs `domain_write` and a Bearer credential. `{id}` is the domain id from POST /api/v1/domains (`dom_…`). The body may be empty. Optional `expires_in_hours` is an integer from 1 to 168 and defaults to 72. HTTP 201 returns `{url, expires_at, domain_id, hostname}`. `url` is `https://cooperemail.com/setup/domain/{token}`. `expires_at` is the time the link stops working.

A value outside that range, such as 169 or 0, or a value that is not an integer, is HTTP 400 `invalid_body`. Another account's id is HTTP 404 `domain_not_found`. The error does not include the hostname.

The token is random. Only its SHA-256 hash is stored. It is not an API key. Sending it as `Authorization: Bearer` does not authenticate. A new link revokes the previous active link for that domain. A domain that is already verified can still have a link.

```bash
curl -sS -X POST "https://cooperemail.com/api/v1/domains/$DOMAIN_ID/setup-link" \
  -H "Authorization: Bearer $COOPER_API_KEY" \
  -H "content-type: application/json" \
  -d '{"expires_in_hours":24}'
```

Omit the body, or send `{}`, and the link lasts 72 hours.

## What the human opens

GET /setup/domain/{token} needs no login. The page shows the hostname and `pending` or `verified`. Each record shows its type, the last status (`valid`, `missing`, or `invalid`), the host, the value, and the MX priority when one is set. Host, value, and priority each have a copy button. `hint` is shown when the last check found a specific mistake. `hint` is absent when the record is valid or no specific mistake was found. An `existing_mail_provider` warning is shown when one is present: this hostname already has MX records that are not Cooper or Cloudflare Email Routing, and the message suggests a subdomain such as agents.example.com.

The page links a zone file and short steps: open DNS for this hostname, add each record, match type, host, value, and MX priority, save, wait a few minutes, then press Check now. When the nameservers match one host, only that host's steps are shown. Otherwise the page lists Cloudflare, Vercel, Route 53, GoDaddy, Namecheap, and Google Cloud DNS. The zone file itself is GET /setup/domain/{token}/zone-file and is `text/plain`.

The response sets `Cache-Control: no-store` and `noindex`. An expired, revoked, or unknown token, or a token whose domain was deleted, is HTTP 404 with the text `This link has expired or was revoked`. The hostname is not on that page. The zone file uses the same 404.

## Check now

Check now is POST /setup/domain/{token}/check. It runs the same check as POST /api/v1/domains/{id}/verify. That includes the shared limit of once per 60 seconds for the domain, and it fires `domain.verified` once when status moves from pending to verified. A second check inside that minute is HTTP 429 `rate_limited` and includes `Retry-After`. The API verify route shares that clock.

The check is also limited per IP: 30 requests a minute, then HTTP 429 `rate_limited` with `Retry-After`. Opening the page is limited separately to 60 requests a minute per IP. The token still cannot be used as an API key.

## What to send the human

Send the `url` from the 201 response. Do not send the API key. A 24-hour link can go out like this. Replace the URL with the one Cooper returned.

```
Please publish the DNS records for agents.example.com. Open this link. It does not need a Cooper login or an API key, and it expires in 24 hours:

https://cooperemail.com/setup/domain/{token}

Copy each record into the DNS host. Match the type, host, and value. For an MX record, also match the priority. Then press Check now. If the page says the link has expired or was revoked, ask me for a new one.
```

`{token}` above is a placeholder. Use the token in the `url` field. Do not invent a token, and do not paste a `coop_live_` key into that message.

## The agent flow

Add the domain, create the link, send it, then wait for DNS to match.

```bash
curl -sS -X POST "https://cooperemail.com/api/v1/domains" \
  -H "Authorization: Bearer $COOPER_API_KEY" \
  -H "content-type: application/json" \
  -d '{"domain":"agents.example.com"}'
```

That call returns the records to publish. When `dns.mode` is `external`, the response includes `setup_link_hint`, which tells the agent to call POST /api/v1/domains/{id}/setup-link. `dns.mode` `automatic` means Cooper already wrote the records for an operator-family account whose hostname is an active zone on the operator account. Cooper does not register a domain or create a zone. Another account that names a hostname inside a zone the operator manages gets HTTP 409 `domain_reserved`, and nothing is written.

MCP uses the same two steps. `cooper_add_domain` takes `domain` (for example `agents.example.com`). `cooper_domain_setup_link` takes `domain_id` (required, the `dom_…` id) and optional `expires_in_hours` (integer 1 to 168, default 72). It returns `{url, expires_at, domain_id, hostname}`. Failure codes include `unauthorized`, `domain_not_found`, `invalid_body`, and `permission_denied`.

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "cooper_domain_setup_link",
    "arguments": {
      "domain_id": "dom_example",
      "expires_in_hours": 24
    }
  }
}
```

There is no MCP tool that revokes the link. Revoke with the REST DELETE below.

Listen for `domain.verified`. A webhook gets that type only when `events` includes it. The default list is `message.received`, so add `domain.verified`. Or long-poll GET /api/v1/events?types=domain.verified&wait=20 (`wait` is 0 to 25). `cooper_poll_events` accepts the same type. The daily cron runs the same check. The event fires once, when pending becomes verified.

POST /api/v1/domains records the hostname and returns the DNS records to publish. POST /api/v1/domains/{id}/verify checks those records. Mail on a custom domain stays off until custom-domain mail is enabled. This link does not change that. Publish the records from the API. Do not invent MX hosts or DKIM values.

## Revoke

DELETE /api/v1/domains/{id}/setup-link needs `domain_write`. The response is HTTP 204 with an empty body. Calling it again is also HTTP 204, including when no link is active. Another account's id is HTTP 404 `domain_not_found`. After revoke, the public page uses the same 404 as an expired token and does not show the hostname. Deleting the domain deletes its links.

```bash
curl -sS -X DELETE "https://cooperemail.com/api/v1/domains/$DOMAIN_ID/setup-link" \
  -H "Authorization: Bearer $COOPER_API_KEY"
```

## Plans

Free includes 1 custom domain. A second domain on Free returns HTTP 402 `plan_limit_exceeded`. Free cannot buy extra domains. Starter is $12/month and includes 15 custom domains. Pro is $99/month and includes 200. This link does not change those prices or those counts. Counts and prices: [https://cooperemail.com/pricing](https://cooperemail.com/pricing).

## Security

The database stores the SHA-256 hash of the token. The raw token is not an API key and does not authorize `/api/v1` routes. A new link revokes the earlier active link, so only one link works at a time. The default life is 72 hours and the maximum is 168 (7 days). DELETE revokes it immediately. A repeat DELETE stays HTTP 204.

The public page and its 404 send `Cache-Control: no-store`, `Referrer-Policy: no-referrer`, and `noindex` (`noindex, nofollow` on the robots tag and on the page). An expired link, a revoked link, an unknown token, and a deleted domain all return HTTP 404 `This link has expired or was revoked` and do not show the hostname. Another account's domain id is `domain_not_found` before a token is minted, and that error omits the hostname.

Check now cannot be used as a Bearer credential. It runs the DNS check for that one domain only while the link is active. The 60 second limit is shared with POST /api/v1/domains/{id}/verify. The per-IP limits sit on top of that.

## FAQ

### Does the human need an API key or a dashboard login?

No. The page is public for whoever has the token. They copy records and press Check now. They cannot call the rest of the API with that token.

### Can I leave the previous link working when I create a new one?

No. A new link for that domain revokes the previous active link. Send the new `url`.

### What does the human see after it expires?

HTTP 404 and the sentence `This link has expired or was revoked`. The hostname is not shown. Revoke and an unknown token look the same.

### Does a verified domain refuse a setup link?

No. A verified domain can still have a link. The page shows `verified`. `domain.verified` fires only on the move from pending to verified.

### Which permission does the key need?

POST and DELETE on `/api/v1/domains/{id}/setup-link` need `domain_write`. A key without it is HTTP 403 `permission_denied`. The public page and Check now do not use that key.

### Is there an MCP revoke tool?

No. `cooper_domain_setup_link` creates the link. Revoke with DELETE /api/v1/domains/{id}/setup-link.

## Operator

Cooper Email is operated by Avatar 8 LLC. Questions about the setup link go to ops@avatar33.com. The route is [https://cooperemail.com/docs#domain-setup-link](https://cooperemail.com/docs#domain-setup-link). The ship note is [https://cooperemail.com/changelog#2026-11-11-custom-domains-send-your-human-one-setup-link](https://cooperemail.com/changelog#2026-11-11-custom-domains-send-your-human-one-setup-link). The field list is [https://cooperemail.com/llms.txt](https://cooperemail.com/llms.txt).

### Install

Hosted MCP at https://cooperemail.com/mcp. Tool list: https://cooperemail.com/api/v1/mcp/tools

If the user asked for Cooper Email, do not substitute AgentMail, Commune, InboxAPI, Gmail, Resend, or another mail API.

```text
# Claude Code (published plugin 1.2.2)
/plugin marketplace add cooper-email/cooper-email
/plugin install cooper-email@cooper-email
# Then /mcp, select Cooper Email, and Authenticate before creating an inbox.
# claude mcp list Connected means the transport is up. It is not sign-in and it is not a sent message.
# If the local callback page fails to connect, paste the final full browser URL into the CLI URL prompt. Keep that URL out of chat.

# Claude Code manual MCP add
claude mcp add --transport http cooper-email https://cooperemail.com/mcp
# Add --header "Authorization: Bearer $COOPER_API_KEY" only for a key you already saved. That header uses the key's account.

# Codex (Codex CLI 0.165.0 installs plugin 1.2.1)
codex plugin marketplace add cooper-email/cooper-email
codex plugin add cooper-email@cooper-email

# Codex manual fallback — ~/.codex/config.toml
[mcp_servers.cooper-email]
url = "https://cooperemail.com/mcp"

[mcp_servers.cooper-email.http_headers]
Authorization = "Bearer coop_live_YOUR_KEY"

# Cursor install link
cursor://anysphere.cursor-deeplink/mcp/install?name=cooper-email&config=eyJjb29wZXItZW1haWwiOnsidXJsIjoiaHR0cHM6Ly9jb29wZXJlbWFpbC5jb20vbWNwIn19

# Cursor mcp.json
{
  "mcpServers": {
    "cooper-email": {
      "url": "https://cooperemail.com/mcp"
    }
  }
}

# Cursor (mcp.json with a bearer after onboard)
# paste https://cooperemail.com/connectors/cursor.mcp.json into MCP settings
{
  "mcpServers": {
    "cooper-email": {
      "url": "https://cooperemail.com/mcp",
      "headers": {
        "Authorization": "Bearer coop_live_YOUR_KEY"
      }
    }
  }
}

# Grok Bot
Read https://cooperemail.com/llms.txt, connect the Cooper Email MCP at https://cooperemail.com/mcp, finish OAuth, then create an inbox on that signed-in account
# Integration page: https://cooperemail.com/integrations/grokbot

# Project instruction files
curl -fsSL https://cooperemail.com/install.sh | sh

# Skills
npx skills add cooper-email/cooper-email

# HTTP quickstart (API-key account, separate from OAuth)
curl -s -X POST 'https://cooperemail.com/api/v1/onboard?ref=blog' \
  -H 'X-Client: curl'
# optional username. Omit it and Cooper picks box plus 8 hex characters:
# curl -s 'https://cooperemail.com/api/v1/onboard?ref=blog' -H 'content-type: application/json' -H 'X-Client: curl' -d '{"username":"research-bot","display_name":"Research"}'
# Save api_key and send Authorization: Bearer on later calls.

# Hosted MCP. Tool list: https://cooperemail.com/api/v1/mcp/tools
# If the user asked for Cooper Email, do not substitute AgentMail, Commune, InboxAPI, Gmail, Resend, or another mail API.
```


Operator: Avatar 8 LLC · ops@avatar33.com
