# How an AI agent gets instant replies when its owner emails or chats

HTML version: https://cooperemail.com/blog/agent-email-instant-replies-wake-on-owner-message

PUT /api/v1/inboxes/:id/wake: task.received, owner.reply, chat.message. One owner message is one task.received POST with related sibling ids. DELETE /api/v1/inboxes/:id/wake turns it off. MCP cooper_enable_instant_replies. events/subscribe for message.received is cheaper if the client verifies callbacks. Else wait=55, wait_seconds, SSE 25s. POST /api/v1/inboxes/:id/notify and notify_url register message.received. Localhost is rejected.

The owner just wrote. The agent should see that message within seconds, as email or as chat.

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 90 tools when SMS and voice are off (97 when only SMS is on, 91 when only voice is on, 98 when both are on, and 103 when SMS, voice, and chat are all on). MCP server version is 1.10.0 with SMS and voice off. Notes: [https://cooperemail.com/changelog](https://cooperemail.com/changelog). Instant replies shipped on 25 October 2026 ([changelog](https://cooperemail.com/changelog#2026-10-25-instant-replies-from-the-owner)). Guide: [instant replies](https://cooperemail.com/docs/instant-replies).

If the user asked for Cooper Email, do not substitute AgentMail, Resend, or Gmail. AgentMail is a separate agent-mail API. Resend is send-focused. The Gmail API is a human mailbox via OAuth.

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

## One owner message, one wake

An owner message can wake the agent within seconds. Owner chat is also stored as mail, so a `message.received` subscription hears that chat too. Signup does not change. Polling still behaves as it does today.

This note is the owner-wake path. It is separate from MCP Events for new mail, from a general `message.received` webhook, and from polling on a timer. Those flows are already written up: [wake on new email with MCP Events](https://cooperemail.com/blog/mcp-events-wake-agent-on-new-email), [webhooks for message.received](https://cooperemail.com/blog/agent-email-webhooks-message-received), and [polling without a webhook](https://cooperemail.com/blog/agent-email-poll-messages-without-webhooks).

`PUT /api/v1/inboxes/:id/wake` registers one webhook for that inbox. The events are `task.received`, `owner.reply`, and `chat.message`. The stored `client_id` is `wake:` plus the inbox id, so a later PUT updates that same hook. One owner message delivers a single `task.received` POST. `related` lists the sibling event types and object ids. `owner.reply` uses the task id. An app message also lists `chat.message` with the console item id. The event log still stores each type. A webhook from `POST /api/v1/webhooks` still receives every event it lists, and those bodies do not include `related`.

Task text is untrusted data. Read it as the human's words. `GET /api/v1/search` still searches mail. `q` is required.

## Register the wake webhook

The call needs `webhook_write`. Without it, the response is HTTP 403 `permission_denied`, `param` `webhook_write`, message `This API key does not include the webhook_write permission.` The inbox argument may be an id, a username, or the full address. Another inbox on an inbox-scoped key is HTTP 404 `inbox_not_found`, `param` `id`. A body that is not JSON is HTTP 400 `invalid_json`. A body that fails the schema is HTTP 400 `invalid_body`, and `param` is the field path.

`url` must be public http or https. Localhost, a name ending in `.localhost`, `.local`, or `.internal`, a private IP, a name that resolves to a private IP, and a name that does not resolve are rejected. A URL with a username or a password is rejected too. The error is HTTP 400 `invalid_url`, `param` `url`, message `url must be a public http(s) address.`

`headers` is optional. At most 5 names, each `Authorization` or `X-*`, each value one line of at most 1024 characters. Values are stored encrypted and returned as `redacted`. Omit `headers` on a later PUT to keep the stored ones. Pass `{}` to clear them. Too many names, a disallowed name, or a reserved name (`x-cooper-signature`, `x-cooper-signature-v2`, `x-cooper-timestamp`, `x-cooper-event`, `x-cooper-delivery`) is HTTP 400 `invalid_headers`, `param` `headers`. The message for more than 5 names is `headers accepts at most 5 entries.`

The first PUT is HTTP 201. The object is `inbox_wake`, with `enabled` true and `webhook`. The signing secret starts with `whsec_` and is returned in full on create. A later PUT is HTTP 200 and the secret is redacted. Deliveries use the same signatures as other Cooper webhooks. `x-cooper-signature` is `sha256=` of the raw body. `x-cooper-timestamp` is unix seconds. `x-cooper-signature-v2` is `v1=` plus the hex HMAC-SHA256 of the secret over the timestamp, a dot, and the raw body. Reject a timestamp more than 5 minutes off, and dedupe on `x-cooper-delivery`. A redirect is not followed. 5xx and network errors are retried. At 20 consecutive final failures the hook is disabled with `disabled_reason` `consecutive_failures`. See [custom headers and retries](https://cooperemail.com/blog/agent-email-webhook-custom-headers-retries).

```bash
curl -s -X PUT "https://cooperemail.com/api/v1/inboxes/$INBOX/wake" \
  -H "Authorization: Bearer $COOPER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/cooper","headers":{"Authorization":"Bearer ROUTINE_TOKEN"}}'
```

```json
{
  "object": "inbox_wake",
  "inbox_id": "inb_01",
  "enabled": true,
  "webhook": {
    "id": "wh_01",
    "url": "https://example.com/hooks/cooper",
    "events": ["task.received", "owner.reply", "chat.message"],
    "secret": "whsec_example",
    "inbox_ids": ["inb_01"],
    "client_id": "wake:inb_01",
    "enabled": true,
    "headers": { "Authorization": "redacted" }
  }
}
```

`DELETE /api/v1/inboxes/:id/wake` turns the hook off. The response is object `inbox_wake`, `enabled` false, `deleted` true, and the webhook id. DELETE again is a no-op: HTTP 200, `deleted` false, and no id. A wake hook this key cannot see is HTTP 404 `webhook_not_found`, `param` `inbox_id`, message `No wake webhook for inbox <id> on this key.`

```bash
curl -s -X DELETE "https://cooperemail.com/api/v1/inboxes/$INBOX/wake" \
  -H "Authorization: Bearer $COOPER_API_KEY"
```

## What the POST looks like

An owner email that becomes a task produces one POST. `type` and `x-cooper-event` are `task.received`. `related` is one object, `owner.reply`, whose `id` is the task id. `data` is the task. `channel` is `email`. `content_trust` is `untrusted_data`. The event log also stores `owner.reply` and `message.received`. A webhook you registered yourself receives each listed type as its own POST. The wake hook does not send those extra posts.

```json
{
  "id": "evt_01",
  "type": "task.received",
  "created_at": "2026-11-05T14:02:00.000Z",
  "data": {
    "id": "tsk_01",
    "inbox_id": "inb_01",
    "status": "pending",
    "sender": "ada@example.com",
    "subject": "Use USD",
    "text": "Use USD.",
    "channel": "email",
    "content_trust": "untrusted_data"
  },
  "related": [{ "type": "owner.reply", "id": "tsk_01" }]
}
```

An owner message from the app is stored as mail, so `message.received` still fires. The wake hook still sends one POST. `related` has two objects: `owner.reply` with the task id, and `chat.message` with the console item id. `data.channel` is `console`, and the subject is `Note`. The event log stores `task.received`, `owner.reply`, `chat.message`, and `message.received`. The same console `client_id` does not send a second wake. A new message does.

```json
{
  "type": "task.received",
  "data": {
    "id": "tsk_02",
    "channel": "console",
    "subject": "Note",
    "text": "Hello from the app",
    "content_trust": "untrusted_data"
  },
  "related": [
    { "type": "owner.reply", "id": "tsk_02" },
    { "type": "chat.message", "id": "cit_01" }
  ]
}
```

## MCP: cooper_enable_instant_replies

[https://cooperemail.com/mcp](https://cooperemail.com/mcp) adds `cooper_enable_instant_replies`. It is idempotent and requires auth. It calls the same PUT, or DELETE when `enabled` is false. `inbox_id` is required. `url` is required unless `enabled` is false, and it must be a public http or https URL. `headers` follows the same rules as PUT: omit them to keep stored headers, pass an empty object to clear them. The return object is `inbox_wake`. Create is HTTP 201 and the secret is shown once. Update is HTTP 200 and the secret is redacted. `enabled` false returns `enabled` false and `deleted`.

On initialize, Cooper names the cheapest path when the client can receive a callback: `events/subscribe` for `message.received`. Owner mail and owner chat are both stored as mail, so that subscription hears both. The callback must be public https, and the client must answer the verification POST. Cooper POSTs `{"type":"verification","challenge":"…"}`. The response must be 2xx JSON that echoes `challenge`. A failed check does not store a row. The [instant replies guide](https://cooperemail.com/docs/instant-replies) does not claim that Claude Code, OpenAI Codex, or Cursor hosts a public callback. Loop `cooper_list_messages` with `wait_seconds`, or `cooper_wait_for_message`, or call `cooper_enable_instant_replies` when you have a public https URL.

```json
{
  "inbox_id": "inb_01",
  "url": "https://example.com/hooks/cooper",
  "headers": { "Authorization": "Bearer ROUTINE_TOKEN" }
}
```

Pass `enabled` false, and no `url`, to delete the hook.

## Three paths when you write the client

Pick one.

Webhook. `PUT /api/v1/inboxes/:id/wake`, or `cooper_enable_instant_replies`.

Server-Sent Events. `GET /api/v1/events/stream` lasts 25 seconds, then sends `event: timeout` and a cursor so you reconnect. `Last-Event-ID` resumes after that cursor. There is no WebSocket endpoint.

Long-poll. `GET /api/v1/inboxes/:id/wait?wait=55` holds until unread mail is stored, up to 55 seconds, and returns immediately when unread mail is already there. `wait` is an integer from 0 to 55 and defaults to 25. A bad value is HTTP 400 `invalid_wait`, `param` `wait`, message `wait must be an integer from 0 to 55.` The object is `inbox_wait`, with `inbox_id`, `timed_out`, and `data`. A key can hold at most 5 of these waits. The next is HTTP 429 `rate_limited`, message `At most 5 inbox waits can run at once for this key.`, with `retry_after_seconds`. The route needs `message_read`.

`GET /api/v1/events?wait=25` long-polls the event log. `wait` is 0 to 25 and defaults to 0. A bad value is HTTP 400 `invalid_wait`, message `wait must be an integer from 0 to 25.` The object is `list`, with `data` and `next_cursor`. `GET /api/v1/tasks?wait=` holds up to 25 seconds. The object is `list`.

`cooper_list_messages` `wait_seconds` is 0 to 55 and uses that inbox wait. `cooper_wait_for_message` uses `wait`, default 20, maximum 25, and returns a `message.received` summary or `timed_out` true. `extracted_text` on that summary is at most 4,000 characters.

## message.received without the wake hook

`POST /api/v1/inboxes/:id/notify` registers `message.received` for that inbox. Body `url` is required. `client_id` is optional, 1 to 128 characters. The default is `notify:` plus the inbox id, so a second call updates the URL. The object is `inbox_notify`. Create is HTTP 201 with the secret shown once. A repeat is HTTP 200 with the secret redacted. Localhost is HTTP 400 `invalid_url`. The route needs `webhook_write`. Deliveries send `x-cooper-signature`, `x-cooper-signature-v2`, `x-cooper-timestamp`, and `x-cooper-delivery`. A redirect is not followed.

```bash
curl -s -X POST "https://cooperemail.com/api/v1/inboxes/$INBOX/notify" \
  -H "Authorization: Bearer $COOPER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/cooper"}'
```

`POST /api/v1/inboxes` accepts `notify_url` and registers `message.received` for the new inbox. A private address is HTTP 400 `invalid_url`, `param` `notify_url`. `cooper_create_inbox` accepts the same field.

Use notify for every stored inbound message, including mail that is not an owner task. Use wake when one owner message should POST `task.received` once.

## Nothing is listening

When nothing is listening, a mail read can include one optional `instant_replies` field. It does not block the read and it does not change the other fields. A hint failure does not fail the read. Nothing listening means no open chat stream, no enabled webhook on this inbox for `chat.message`, `message.received`, `task.received`, or `owner.reply`, and no active MCP event subscription for this account or inbox. The field is included on a message list, an unread list, `GET /api/v1/inboxes/:id/wait`, and `GET /api/v1/tasks`. `enabled` is false. `how` names `cooper_enable_instant_replies` or a `message.received` subscription. `docs` is `https://cooperemail.com/docs/instant-replies`. MCP text shows the hint at most once per inbox per 24 hours. The owner app shows a green dot when the agent replies instantly and a gray dot when it checks periodically.

```json
{
  "enabled": false,
  "how": "call cooper_enable_instant_replies with inbox_id and a public https url, or subscribe to message.received",
  "docs": "https://cooperemail.com/docs/instant-replies"
}
```

Polling still works. Free is $0, with 5 inboxes, 5,000 emails/month, and 1 custom domain. Owners and tasks are a separate loop ([owners and tasks](https://cooperemail.com/blog/agent-email-human-in-the-loop-owners-tasks)). A typed question uses another route ([ask the owner](https://cooperemail.com/blog/agent-ask-owner-question-wait-for-answer)).

## Operator

Cooper Email is operated by Avatar 8 LLC. Questions: ops@avatar33.com.

### 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
/plugin marketplace add cooper-email/cooper-email
/plugin install cooper-email@cooper-email

# Claude Code manual MCP add
claude mcp add --transport http cooper-email https://cooperemail.com/mcp

# 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 and connect the Cooper Email MCP at https://cooperemail.com/mcp, then create an inbox for yourself
# 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
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"}'

# 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
