# How an AI agent finds out why its Cooper Email API call was refused

HTML version: https://cooperemail.com/blog/agent-email-request-log-refused-calls

GET /api/v1/request-log is the agent email API error log for refused calls, newest first. A row is stored only after a live key or OAuth token was accepted and the response was an HTTP 4xx ApiError. Unauthenticated 401 responses, HTTP 2xx, and HTTP 5xx are not stored. Filters: status, code, api_key_id, inbox_id, since, limit (1–100, default 25), cursor. A full-access account key sees the account. A restricted or inbox-scoped key sees only its own key. MCP: cooper_list_request_refusals.

The tool result said the call failed, then the transcript moved on. When you need to answer why did my agent's email API call fail, read the error Cooper just returned, then the agent email API error log of recent refusals.

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 88 tools when SMS and voice are off (95 when only SMS is on, 89 when only voice is on, 96 when both are on, and 101 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). This log shipped on 2 November 2026: [https://cooperemail.com/changelog#2026-11-02-see-why-your-agent-s-api-calls-were-refused](https://cooperemail.com/changelog#2026-11-02-see-why-your-agent-s-api-calls-were-refused). Reference: [https://cooperemail.com/docs/reference/request-log](https://cooperemail.com/docs/reference/request-log).

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

## Read the live 4xx before you search the log

Every Cooper error is JSON. Branch on `code`. The HTTP status is the response status. The body always has `type`, `code`, `message`, and `docs_url`. `param` is included when the failure names a field or a permission, and omitted when it does not. `docs_url` is `https://cooperemail.com/docs#` plus the code.

```json
{
  "error": {
    "type": "authentication_error",
    "code": "permission_denied",
    "message": "This API key does not include the inbox_create permission.",
    "param": "inbox_create",
    "docs_url": "https://cooperemail.com/docs#permission_denied"
  }
}
```

`type` matches the status: `invalid_request` (400), `authentication_error` (401 and 403), `payment_required` (402), `not_found` (404), `conflict` (409), `rate_limit` (429), `unsupported_media` (415), or `internal_error` (500, 502, and 503). A 403 `permission_denied` is `authentication_error`.

HTTP 429 `rate_limited`, and an HTTP 503 that sends `Retry-After`, also include `retry_after_seconds` and `resets_at`. `retry_after_seconds` is that header's integer, at least 1. `resets_at` is that many seconds later, in UTC. Both are omitted when no retry time is known. An inbox, email, or custom-domain cap is HTTP 402 `plan_limit_exceeded` and adds `upgrade_url`, `plan`, `resource`, `limit`, `used`, and `next_plan` on the live JSON. The log keeps `type`, `code`, `param`, and `message` only. Keep the live body for `retry_after_seconds`, `resets_at`, or `upgrade_url`.

A body that fails schema validation is HTTP 400 `invalid_body`. `param` is the field path, dots between segments. That JSON is returned and is not written to the log. An HTTP 4xx thrown after the key was accepted, such as `invalid_status` or `permission_denied`, is recorded.

## Open the refused-request log

`GET /api/v1/request-log` lists refused calls for the signed-in account, newest first by `created_at` then id. Any live key can call it, including a restricted or inbox-scoped key. A missing or unrecognized key is HTTP 401 `unauthorized`. A revoked key is HTTP 401 `api_key_revoked`. An expired key is HTTP 401 `api_key_expired`. Those 401 responses are not stored. The MCP tool with the same list is `cooper_list_request_refusals`.

A row is stored only when a live API key or OAuth token was accepted and the response was an HTTP 4xx ApiError (status 400 through 499, including 400, 402, 403, 404, 409, and 429). HTTP 2xx is not stored. HTTP 5xx is not stored, including 503. Recording is best-effort and does not change the HTTP response. A failed write still returns the original 4xx, and that refusal can be missing from the list.

Each account keeps at most 1000 rows per UTC day. Further refusals that day are dropped. The 4xx is still returned. Rows older than 7 days are deleted when that account next writes or lists. Another account gets HTTP 200 and an empty `data` array.

The row stores method, path, status, and error `type`, `code`, `param`, and `message`. The message is cut at 300 characters. An email address, a `coop_live_` or `coop_test_` token, or a Bearer token in the message becomes `[redacted]` before that cut. The path keeps ids and drops the query string. A path segment that contains `@` or a key token is stored as `:id`. `inbox_id` is set from `/inboxes/{id}` when that segment is an inbox id. `batch`, `send-mode`, and `delete` are not inbox ids. The row also stores `api_key_id` or `oauth_client_id`, `source` (`rest` or `mcp`), and `mcp_tool` (the tool name, at most 80 characters) for an MCP call. `mcp_tool` is null on REST. Request bodies, query strings, Authorization headers, API keys, tokens, email content, and recipient addresses are not stored.

A full-access account key (`permissions` null, and not pinned to an inbox) sees the account. A restricted key, or any inbox-scoped key, sees only its own `api_key_id`. An OAuth client sees only its own `oauth_client_id`, and those rows have `api_key_id` null. Debug a sub-agent with a full-access account key or with that sub-agent's own key.

```bash
curl -s "https://cooperemail.com/api/v1/request-log?status=403&code=permission_denied&limit=25" \
  -H "Authorization: Bearer $COOPER_API_KEY"
```

```json
{
  "object": "list",
  "data": [
    {
      "object": "request_refusal",
      "id": "rrf_01",
      "created_at": "2026-11-02T14:00:00.000Z",
      "method": "POST",
      "route": "/api/v1/inboxes",
      "status": 403,
      "error": {
        "type": "authentication_error",
        "code": "permission_denied",
        "param": "inbox_create",
        "message": "This API key does not include the inbox_create permission."
      },
      "api_key_id": "key_01",
      "oauth_client_id": null,
      "inbox_id": null,
      "source": "mcp",
      "mcp_tool": "cooper_create_inbox"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

The list object is `list`. Each row's object is `request_refusal`. `has_more` is true when another row sits past this page, and only then is `next_cursor` set. Otherwise it is null. Pass it back as `cursor`. A cursor that does not decode is HTTP 400 `invalid_cursor` with `param` `cursor`.

## Filters

`status` must be an HTTP code from 400 to 499, or the call is HTTP 400 `invalid_status` with `param` `status`. `code` is 1 to 64 characters of letters, digits, and underscore (HTTP 400 `invalid_code`, `param` `code`). `api_key_id` is 1 to 80 of those same characters (HTTP 400 `invalid_api_key_id`). `inbox_id` is an inbox id, 1 to 128 characters of letters, digits, `.`, `_`, and `-`, and no `@` (HTTP 400 `invalid_inbox`, `param` `inbox_id`). `since` must parse as a timestamp. Cooper stores that instant in UTC. A bad value is HTTP 400 `invalid_date` with `param` `since`. `limit` is an integer from 1 to 100 and defaults to 25 (HTTP 400 `invalid_limit`, `param` `limit`). A restricted key that also passes another key's `api_key_id` still sees only its own rows.

```bash
curl -s "https://cooperemail.com/api/v1/request-log?api_key_id=$KEY_ID&inbox_id=$INBOX_ID&since=2026-11-02T00:00:00.000Z&limit=100" \
  -H "Authorization: Bearer $COOPER_API_KEY"
```

## MCP tool call refused

[https://cooperemail.com/mcp](https://cooperemail.com/mcp) exposes the same list as `cooper_list_request_refusals`. It is read-only. Inputs match the query string: `status`, `code`, `api_key_id`, `inbox_id`, `since`, `limit`, and `cursor`. The tool calls `GET /api/v1/request-log` and returns that JSON. A failed Cooper tool comes back with `isError` true and that error JSON in the text.

An MCP refusal is stored with `source` `mcp` and `mcp_tool` set to the tool name. `method` and `route` are the HTTP call the tool made. `cooper_create_inbox` without `inbox_create` is `POST` `/api/v1/inboxes`, status 403, `mcp_tool` `cooper_create_inbox`. The route is not `/mcp`. The API key is not in the tool text or the row.

```json
{
  "status": 403,
  "code": "permission_denied",
  "api_key_id": "key_01",
  "inbox_id": "inb_01",
  "since": "2026-11-02T00:00:00.000Z",
  "limit": 25
}
```

Pass those fields to `cooper_list_request_refusals`. Omit a field to skip that filter. Call again with `cursor` set to `next_cursor` while `has_more` is true.

## Fix the common refusals

**Bad scope.** `permission_denied` means the key was accepted, then rejected. `param` is the missing permission, or `permissions` when the route needs a full-access key. A missing grant says `This API key does not include the <name> permission.` `GET /api/v1/stats` needs full access: the message is `This route requires a full-access API key.` and `param` is `permissions`. An inbox-scoped key on an account-wide route, including `POST /api/v1/onboard` and `POST /api/v1/inboxes`, says `An inbox-scoped API key cannot call this route.` `param` is the route permission (`inbox_create` for those two).

`permissions` null is full access inside the inbox scope. An empty array is not. OAuth is full access: `GET /api/v1/me` and `cooper_whoami` return `auth_type` `oauth` and `key` null. On an API key, `key` has `id`, `name`, `prefix`, `inbox_id`, `permissions`, and `expires_at`. `inbox_id` null means the key is not pinned. Compare those with `param`. Accepted names are `inbox_read`, `inbox_create`, `inbox_update`, `inbox_delete`, `message_read`, `message_send`, `message_update`, `message_delete`, `draft_write`, `draft_send`, `webhook_read`, `webhook_write`, `domain_read`, `domain_write`, `list_read`, `list_write`, `owner_manage`, `task_read`, `task_reply`, `key_read`, `key_write`, `workspace_manage`, `billing_read`, and `billing_write`. An unknown name is HTTP 400 `invalid_permission` with `param` `permissions`. Mint a wider key over HTTP with `POST /api/v1/keys` or `POST /api/v1/inboxes/:id/keys`. The MCP server does not mint keys. See [scoped API keys](https://cooperemail.com/blog/agent-email-scoped-api-keys-sub-agents).

A scoped key that names another inbox is HTTP 404 `inbox_not_found`, `param` `id`, message `No inbox matching "<id>" for this API key.` That 404 is stored. It is not `permission_denied`. Use this key's inbox id, username, or address.

**Invalid params.** Change the field named by `param`. On `GET /api/v1/inboxes`, `invalid_status` means `status` was not `active` or `paused`. On this log, `invalid_status` means the filter was not an HTTP 4xx code. `invalid_limit`, `invalid_cursor`, `invalid_date`, `invalid_code`, `invalid_api_key_id`, and `invalid_inbox` are the filters above. `invalid_body` names a body field and is not in the log, so keep that live response. Call the original route again. A 400 is not a retry on a timer.

**Rate limits.** HTTP 429 `rate_limited` is stored. One key can hold at most 5 inbox waits at once, shared by `GET /api/v1/inboxes/:id/wait` and wait-for-code. The next call says `At most 5 inbox waits can run at once for this key.` and sends `Retry-After` 1. `POST /api/v1/domains/:id/verify` runs at most once every 60 seconds. The message is `Domain verification can run at most once every 60 seconds.` Wait until `resets_at` on the live error. The log row has status 429 and code `rate_limited` and does not include `retry_after_seconds`. See [wait for a code](https://cooperemail.com/blog/agent-email-wait-for-code).

**Plan limits.** HTTP 402 `plan_limit_exceeded` is stored. The live error includes `upgrade_url`. The stored message names the plan, the cap, and the usage. A sub-account past its inbox quota is HTTP 402 `workspace_quota_exceeded`, with `resource`, `limit`, `used`, and `workspace_id` on the live error. See [billing](https://cooperemail.com/blog/agent-email-billing-checkout-upgrade).

## What the log will not explain

A successful send, an HTTP 5xx, and a call Cooper could not tie to an account are not rows. `GET /api/v1/search` searches mail, not this log. `q` is required. An empty `q` is HTTP 400 `missing_query`. Prefer a `message.received` webhook when you have a public HTTPS URL.

## Related

- [Onboard in one request](https://cooperemail.com/blog/agent-email-onboard-one-request)
- [Scoped API keys for sub-agents](https://cooperemail.com/blog/agent-email-scoped-api-keys-sub-agents)
- [OAuth connected apps](https://cooperemail.com/blog/agent-email-oauth-connected-apps)
- [MCP for Cursor, VS Code, Codex, and Cline](https://cooperemail.com/blog/mcp-email-cursor-vscode-codex-cline)
- [Wait for a verification code](https://cooperemail.com/blog/agent-email-wait-for-code)
- [Billing, checkout, and HTTP 402](https://cooperemail.com/blog/agent-email-billing-checkout-upgrade)

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