Blog ·

How an AI agent reads mail volume and bounce rates on Cooper Email

GET /api/v1/metrics/events buckets message.received, message.sent, message.bounced, and message.complained by UTC hour or day (period hour|day, default day). start and end default to the last 30 days and cannot exceed 90 days (HTTP 400 invalid_range). GET /api/v1/metrics/usage returns inbox_count, message_count, thread_count, and storage_bytes. GET /api/v1/metrics/rates returns sent, bounced, complained, bounce_rate, and complaint_rate (0 when sent is 0). Requires message_read. MCP: cooper_get_metrics with kind events|usage|rates.

An agent that reports volume, storage, or a bounce rate should read counts. Downloading every body to add them up wastes the context window, and a held message withholds that text until release. Cooper Email stores the mail and answers three read routes with numbers only. This page is how to call them.

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. Routes: https://cooperemail.com/docs. Hosted MCP: https://cooperemail.com/mcp (67 tools). Notes: https://cooperemail.com/changelog.

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

Volume by UTC hour or day

GET /api/v1/metrics/events?types=&start=&end=&period=&inbox_id= returns UTC buckets. Each bucket is a start time plus a counts object. The response object is metrics.

types is a comma-separated list of message.received, message.sent, message.bounced, and message.complained. Omit types and the series is message.received,message.sent. message.received follows direction inbound. message.sent follows direction outbound. message.bounced and message.complained follow those labels. A missing label is 0, so a quiet hour still shows the key with a zero. An unknown type, including message.held, is HTTP 400 invalid_type.

period is hour or day. The default is day. A day bucket starts at UTC midnight (2026-10-10T00:00:00.000Z). An hour bucket starts at the UTC hour. Anything else, such as week, is HTTP 400 invalid_period.

start and end are ISO-8601. start is inclusive and end is exclusive. Omit both and the window is the last 30 days, ending now. Omit start and it is 30 days before end. Omit end and it is now. The span cannot exceed 90 days, and start must be before end. A range of exactly 90 days is accepted. A longer span, or a start that is not before the end, is HTTP 400 invalid_range. A bad timestamp, such as the word yesterday, is HTTP 400 invalid_date. The param names the bad field.

Every bucket from the floored start through the bucket that contains the instant before end is present, including hours with no mail. The agent does not have to fill gaps.

The call needs message_read. A key that omits it is HTTP 403 permission_denied, and param is message_read. An inbox-scoped key sees only its inbox. inbox_id accepts an inbox id, a username, or the full address. Another account's inbox, or a different inbox on a scoped key, is HTTP 404 inbox_not_found. A sub-account sees only its own mail. A parent key that asks for a sub-account inbox gets that same 404.

The JSON is counts only. It never includes a message body, including held mail. Held mail is still counted: an inbound message labeled held increments message.received for its UTC bucket, and the text stays off the response. A weekly product note labeled cooper-system does not count toward metrics usage, and it is left out of the message.received series. The signed webhook can still carry message.received for that note. Use the label when you filter the event log.

curl -s "https://cooperemail.com/api/v1/metrics/events?types=message.received,message.sent,message.bounced,message.complained&start=2026-10-09T23:00:00.000Z&end=2026-10-10T02:00:00.000Z&period=hour&inbox_id=$INBOX" \
  -H "Authorization: Bearer $COOPER_API_KEY"
{
  "object": "metrics",
  "period": "hour",
  "data": [
    {
      "bucket_start": "2026-10-09T23:00:00.000Z",
      "counts": {
        "message.received": 1,
        "message.sent": 0,
        "message.bounced": 0,
        "message.complained": 0
      }
    },
    {
      "bucket_start": "2026-10-10T00:00:00.000Z",
      "counts": {
        "message.received": 1,
        "message.sent": 2,
        "message.bounced": 1,
        "message.complained": 1
      }
    },
    {
      "bucket_start": "2026-10-10T01:00:00.000Z",
      "counts": {
        "message.received": 0,
        "message.sent": 0,
        "message.bounced": 0,
        "message.complained": 0
      }
    }
  ]
}

period=day from 2026-10-09T00:00:00.000Z to 2026-10-11T00:00:00.000Z returns the 9 October and 10 October buckets. The end instant is exclusive, so 11 October is absent. With types omitted, each counts object has message.received and message.sent only.

{
  "error": {
    "type": "invalid_request",
    "code": "invalid_range",
    "message": "start must be before end, and the range cannot exceed 90 days.",
    "param": "end",
    "docs_url": "https://cooperemail.com/docs#invalid_range"
  }
}

Current inbox, message, thread, and storage totals

GET /api/v1/metrics/usage?inbox_id= is a snapshot, with no time window. It returns inbox_count, message_count, thread_count, and storage_bytes.

storage_bytes is the UTF-8 byte length of text plus html, plus each attachment size_bytes. The field is the size, and the route does not return the message body. Held mail contributes its stored size the same way, still without the text. In the account example below, hello is 5 bytes, the html is 9, the file is 4, and é is 2, so that inbox is 20. A second inbox adds elsewhere (9 bytes). An empty body still counts as a message and adds 0, so the account storage_bytes is 29.

Pass inbox_id, or call with an inbox-scoped key, and the JSON omits domain_count and workspace_count. An account-wide key also receives those two counts, for this account only. A sub-account does not see the parent account or a sibling sub-account. Its own domain_count and workspace_count are its rows, which is why a child with no workspaces of its own reports workspace_count 0 while the parent reports the child workspace. Another account's inbox is HTTP 404 inbox_not_found. A key without message_read is HTTP 403 permission_denied.

curl -s "https://cooperemail.com/api/v1/metrics/usage" \
  -H "Authorization: Bearer $COOPER_API_KEY"
{
  "inbox_count": 2,
  "message_count": 4,
  "thread_count": 3,
  "storage_bytes": 29,
  "domain_count": 1,
  "workspace_count": 1
}

The same route with inbox_id set:

curl -s "https://cooperemail.com/api/v1/metrics/usage?inbox_id=$INBOX" \
  -H "Authorization: Bearer $COOPER_API_KEY"
{
  "inbox_count": 1,
  "message_count": 3,
  "thread_count": 2,
  "storage_bytes": 20
}

How a parent gives a child its own key is in sub-accounts. How to pin a key to one inbox is in scoped API keys.

Bounce rate and complaint rate

GET /api/v1/metrics/rates?start=&end=&inbox_id= returns five numbers for one window: sent, bounced, complained, bounce_rate, and complaint_rate.

sent counts outbound messages in the window. bounced counts messages whose labels include bounced. complained counts messages whose labels include complained. bounce_rate is bounced divided by sent. complaint_rate is complained divided by sent. Both rates are 0 when sent is 0. One bounce and one complaint among three outbound messages comes back as bounce_rate 0.333333 and complaint_rate 0.333333.

start and end follow the same rules as events. The default is the last 30 days. A span over 90 days, or a start that is not before end, is HTTP 400 invalid_range. A bad timestamp is HTTP 400 invalid_date. The response is counts only.

An inbound delivery report is its own stored message. It increments sent only when that row is outbound. The outbound message that the report matches is the row that carries the bounced or complained label. Suppression of the recipient, and HTTP 403 recipient_suppressed on a later send, is a separate list. See screening, suppression, and idempotent sends.

curl -s "https://cooperemail.com/api/v1/metrics/rates?start=2026-10-09T00:00:00.000Z&end=2026-10-10T00:00:00.000Z&inbox_id=$INBOX" \
  -H "Authorization: Bearer $COOPER_API_KEY"
{
  "sent": 3,
  "bounced": 1,
  "complained": 1,
  "bounce_rate": 0.333333,
  "complaint_rate": 0.333333
}

A window with no outbound mail:

{
  "sent": 0,
  "bounced": 0,
  "complained": 0,
  "bounce_rate": 0,
  "complaint_rate": 0
}

MCP: cooper_get_metrics

https://cooperemail.com/mcp exposes the same three reads. The catalog is 67 tools.

cooper_get_metrics takes kind: events, usage, or rates. An unknown kind, such as export, is HTTP 400 invalid_kind (kind must be events, usage, or rates). Optional inbox_id is an id, a username, or an email. start and end apply to events and rates. period (hour or day, default day) and types apply to events. The tool is read-only and needs message_read. The JSON matches the HTTP route, including object metrics for events and the five rate fields for rates. Bodies stay off the result.

{
  "name": "cooper_get_metrics",
  "arguments": {
    "kind": "rates",
    "inbox_id": "inb_01",
    "start": "2026-10-09T00:00:00.000Z",
    "end": "2026-10-10T00:00:00.000Z"
  }
}

Use kind events when you need the buckets, and kind usage when you need the current totals.

Dashboard stats and billing stay on their own routes

GET /api/v1/stats is unchanged. It is dashboard analytics: inboxes, messages, inbound, outbound, webhooks, and by_day. It does not return storage_bytes, bounce_rate, or the UTC event buckets. Keep using it for the dashboard shape you already parse.

Plan, this month's sends, limits, and the upgrade path are GET /api/v1/billing. Checkout and the customer portal are separate posts. A 402 plan_limit_exceeded cites the billing caps. These metric routes do not return a plan name or a Stripe URL. Read billing in check usage and upgrade.

When you need the text, or a wake-up

Metrics never return bodies. When the agent needs message text, call GET /api/v1/search. Account search is covered in full-text search. Held mail stays off search until release, and the metrics count for that message is still only a number.

A signed message.received webhook wakes the agent when mail arrives. Metrics do not replace webhooks. A bucket does not call your URL, and a rate does not tell you that a new message was stored. Register the hook as in inbound webhooks. If you have no public URL, poll or wait as in polling without webhooks. Read the counts on a schedule after the wake-up, or when a report is due.

Other mail products

AgentMail, Resend, and Gmail are other mail paths. AgentMail is an agent-mail API with its own inboxes. Resend sends application mail. The Gmail API typically authorizes a human mailbox and does not create agent inboxes. This page does not describe their counts. Cooper keeps volume, storage, and bounce rate on the same account as the agent inbox.

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

Paste into ChatGPT, Claude, Cowork, or any agent

ChatGPTClaudeCoworkClaude CodeCursor
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).

Your chat agent gets a real inbox in one request. Connect Cooper MCP when available, then paste this into ChatGPT, Claude, or Cowork. Creates a coop_live_ key + username@cooperemail.com.