On this page

zoomfort docs

zoomfort gives an AI agent its own email address. It receives mail and cannot send any. Your agent reads its mail through a small HTTP API.

Quick start

  1. Sign in to your inbox and create an address, such as signup@yourname.zoomfort.com.
  2. Under Settings, API keys, create a key with read:codes. Add read:mailbox if the agent needs to read messages. Choose which addresses it can read. Creating a key asks for your current password again.
  3. Give the agent its address and key. Keep the key in its secret environment as $ZOOMFORT_API_KEY. Start the sign-up, then request the code.

All request examples use https://mail.zoomfort.com/api/v1. These are sample messages and timestamps; replace since with the time you started waiting and from_domain with the service’s sender domain.

Quick start request
curl --get 'https://mail.zoomfort.com/api/v1/code' \
  -H "Authorization: Bearer $ZOOMFORT_API_KEY" \
  --data-urlencode 'to=signup@yourname.zoomfort.com' \
  --data-urlencode 'from_domain=service.example' \
  --data-urlencode 'since=2026-10-13T08:00:00Z' \
  --data-urlencode 'wait=25'
Quick start answer · 200
{
  "code": "482193",
  "message_id": "sample-message",
  "from": "signup@service.example",
  "subject": "Your sign-in code",
  "received_at": "2026-10-13T08:00:05.000Z"
}

The request waits up to 25 seconds and returns an eligible code as soon as it finds one. If no code comes back, follow the nothing-found answers: retry for missing mail, inspect a message with no code, or review failed sender verification.

Addresses

You create each address in the web inbox before it can receive mail. Mail to a name you have not created is refused. There is no catch-all or plus-addressing. Use one address per service so you can retire it independently.

A deleted address is retired and never given to anyone else. Its owner can restore it. Free allows 5 addresses; the $4.99 plan (Coming soon) allows 10, with additional slots described below.

List reachable addresses

GET /addresses lists non-system addresses within the key’s reach. No parameters or read permission are required beyond a valid key. Retired addresses are excluded; check state before using an address.

Address list request
curl --get 'https://mail.zoomfort.com/api/v1/addresses' \
  -H "Authorization: Bearer $ZOOMFORT_API_KEY"
Address list answer · 200
{
  "addresses": [
    {
      "id": 1,
      "address": "signup@yourname.zoomfort.com",
      "local_part": "signup",
      "label": null,
      "kind": "user",
      "state": "active",
      "created_at": 1791878400000,
      "tombstoned_at": null,
      "restored_at": null,
      "sender_lock": 0
    }
  ]
}

API keys

Send the key in the request header Authorization: Bearer $ZOOMFORT_API_KEY. Save it when created or rotated: the full key is shown once. Keys belong to your account. They cannot send mail, change messages, mark important, or administer other accounts.

PermissionAllows
read:codesGet a verification code and wait for one.
read:mailboxList messages, read full or compact messages, and download attachments.
read:statsRead message statistics.

Choose all addresses, including future ones, or a specific list of live addresses. A specific list permanently loses a deleted address, even if you restore it later. The optional main key reaches all addresses, has every read permission and no expiry. It can be rotated but cannot be revoked. It does not manage other keys.

Other keys can have an expiry or no expiry and can be permanently revoked in Settings. An inactive account cannot use its keys. Free allows 10 unrevoked keys; the $4.99 plan (Coming soon) allows 50. The main key and expired keys count until revoked. Existing keys survive a downgrade, but you cannot add one while at the limit.

Rotation in Settings keeps the previous secret working for up to 24 hours, or until expiry, unless you choose to stop it immediately. A second rotation drops the older secret. Creating or rotating in Settings requires a fresh password confirmation valid for five minutes.

Rotate the calling key

POST /keys/self/rotate replaces both secrets immediately. Use the current secret; the overlap secret cannot rotate. No parameters or request body are needed. Save the returned token before making another request.

Self rotation request
curl -X POST 'https://mail.zoomfort.com/api/v1/keys/self/rotate' \
  -H "Authorization: Bearer $ZOOMFORT_API_KEY"
Self rotation answer · 200
{
  "key": {
    "id": "SampleKey001",
    "name": "signup-agent",
    "is_main": false,
    "reach": [
      1
    ],
    "permissions": [
      "read:mailbox",
      "read:codes"
    ],
    "expires_at": null,
    "created_at": 1791878400000,
    "last_used_at": 1791878405000,
    "old_secret_valid_until": null
  },
  "token": "<new key shown once>"
}

The token above is a placeholder for the newly issued key. Self rotation works for any account API key, including the main key.

Get a verification code

GET /code requires read:codes. Always name the sender you expect and pass the time you started waiting so an older sign-in code is not selected.

NameRequiredMeaning
toYesAn address within the key’s reach, such as signup@yourname.zoomfort.com.
from_domainNo; recommendedExpected sender domain. Matches that domain and its subdomains; see sender checks.
sinceNo; recommendedInclusive received-at lower bound. ISO timestamp or Unix milliseconds; defaults to 0 (all retained mail).
waitNoSeconds to wait; defaults to 0. Nonnegative numbers are capped at 25.
require_authNoDefaults to 1: require a verified sender. 0 skips verification, but keeps sender-domain, quarantine and retention filters.
Code request
curl --get 'https://mail.zoomfort.com/api/v1/code' \
  -H "Authorization: Bearer $ZOOMFORT_API_KEY" \
  --data-urlencode 'to=signup@yourname.zoomfort.com' \
  --data-urlencode 'from_domain=service.example' \
  --data-urlencode 'since=2026-10-13T08:00:00Z' \
  --data-urlencode 'wait=25'
Code answer · 200
{
  "code": "482193",
  "message_id": "sample-message",
  "from": "signup@service.example",
  "subject": "Your sign-in code",
  "received_at": "2026-10-13T08:00:05.000Z"
}

The newest eligible code wins. Quarantined mail and content whose cleanup has begun or completed are always excluded. A verified code takes precedence over the following HTTP 404 answers. Nothing-found answers never include a code.

AnswerMeaningAgent action
no_messageNo mail matched the address, time, sender and visibility filters.Retry with a wait, keeping the original start time. Check the address and sender domain.
no_codeMatching mail has no extracted code. The answer includes message_id.Read that message; a service may have sent a link.
sender_not_verifiedMatching mail has a code but fails sender verification. The answer includes message_id.Inspect the message and verification evidence. Keep verification enabled for normal sign-ins.

Other-sender mail does not end the wait. The request polls at most ten times. A broad search can exceed its query budget and return 503 search_limit_exceeded; narrow since and retry.

Read messages

All message endpoints require read:mailbox. Message reads do not mark mail read or change it. Responses are JSON; attachment downloads return bytes. Collection filters apply only to the list and code lookup. By-ID reads can inspect reachable Quarantine messages.

List messages

GET /messages returns newest first, with summaries and metadata rather than full bodies. It includes Inbox and Digest by default. There is no cursor parameter; use a bounded time window.

NameRequiredMeaning
toNoLimit to an address within the key’s reach; otherwise search all reachable addresses.
sinceNoInclusive received-at lower bound: ISO timestamp or Unix milliseconds. Defaults to 0.
limitNoPositive integer; defaults to 50 and is capped at 100 after filtering.
folderNoinbox, digest or quarantine. Quarantine also needs include_quarantined=1.
include_quarantinedNoSet to 1 to include Quarantine; otherwise quarantined messages are excluded.
from_domainNoExpected sender domain or its subdomains, as for code lookup.
require_authNo0 by default; set 1 to require verified senders.
Message list request
curl --get 'https://mail.zoomfort.com/api/v1/messages' \
  -H "Authorization: Bearer $ZOOMFORT_API_KEY" \
  --data-urlencode 'to=signup@yourname.zoomfort.com' \
  --data-urlencode 'since=2026-10-13T08:00:00Z' \
  --data-urlencode 'limit=50'
Message list answer · 200
{
  "messages": [
    {
      "id": "sample-message",
      "from_name": "Sample service",
      "subject": "Your sign-in code",
      "code": "482193",
      "has_attachments": 1,
      "attachment_count": 1,
      "category": null,
      "folder": "inbox",
      "auth_verdict": "pass",
      "to": "signup@yourname.zoomfort.com",
      "from": "signup@service.example",
      "received_at": "2026-10-13T08:00:05.000Z",
      "summary": {
        "who": { "name": "Sample service", "address": "signup@service.example", "org": "service.example" },
        "asks": null, "request": null,
        "deadline": { "stated": null, "text": null },
        "code": "482193", "category": null
      }
    }
  ]
}

The full answer has more fields; these are the ones most agents need.

List rows include summary and summary_json as parsed objects, along with category, folder, code, attachment counts and estimated tokens saved. Purged stubs can still appear with their content cleared.

Read one full message

GET /messages/{id} includes retained text and HTML bodies, headers, sender-check results, classification fields and attachment metadata. It can return an expired stub; bodies and attachments cannot be recovered after cleanup.

NameRequiredMeaning
id (path)YesMessage ID from the list or code answer, within the key’s reach.
Full message request
curl --get 'https://mail.zoomfort.com/api/v1/messages/sample-message' \
  -H "Authorization: Bearer $ZOOMFORT_API_KEY"
Full message answer · 200
{
  "id": "sample-message",
  "from_name": "Sample service",
  "subject": "Your sign-in code",
  "text": "Use 482193 to finish signing in.",
  "code": "482193",
  "auth_verdict": "pass",
  "folder": "inbox",
  "category": null,
  "to": "signup@yourname.zoomfort.com",
  "from": "signup@service.example",
  "received_at": "2026-10-13T08:00:05.000Z",
  "summary": {
    "who": { "name": "Sample service", "address": "signup@service.example", "org": "service.example" },
    "asks": null, "request": null,
    "deadline": { "stated": null, "text": null },
    "code": "482193", "category": null
  },
  "attachments": [{
    "idx": 0,
    "filename": "notes.txt",
    "size": 13,
    "download_url": "/api/v1/messages/sample-message/attachments/0"
  }]
}

The full answer has more fields; these are the ones most agents need.

Read a compact message

GET /messages/{id}?view=compact omits bodies, envelope sender, headers and attachments. It includes from, subject, category, summary, code, folder and auth_verdict. Categories other than verification code, promotion and notification also include summary_json.

NameRequiredMeaning
id (path)YesA reachable message ID.
viewYes for compactSet to compact; otherwise the full message is returned.
Compact message request
curl --get 'https://mail.zoomfort.com/api/v1/messages/sample-message?view=compact' \
  -H "Authorization: Bearer $ZOOMFORT_API_KEY"
Compact message answer · 200
{
  "from": "signup@service.example",
  "subject": "Your sign-in code",
  "category": null,
  "summary_json": {
    "who": { "name": "Sample service", "address": "signup@service.example", "org": "service.example" },
    "asks": null, "request": null,
    "deadline": { "stated": null, "text": null },
    "code": "482193", "category": null
  },
  "summary": {
    "who": { "name": "Sample service", "address": "signup@service.example", "org": "service.example" },
    "asks": null, "request": null,
    "deadline": { "stated": null, "text": null },
    "code": "482193", "category": null
  },
  "code": "482193",
  "folder": "inbox",
  "auth_verdict": "pass"
}

Summaries use rules and sender-authored excerpts, without an LLM call. They include sender details, a request excerpt of at most 160 characters, deadline text and a code when available. who.org is a domain, not a verified company name. Promotion and phishing categories have no structured summary. Classification flags can be null when classification is unavailable. The compact by-ID view does not apply the list’s link filtering.

Download an attachment

GET /messages/{id}/attachments/{idx} downloads an attachment listed in the full message. Use the relative download_url on https://mail.zoomfort.com with the same key header. Downloads use attachment disposition and are not cached publicly.

NameRequiredMeaning
id (path)YesA reachable message ID.
idx (path)YesNonnegative attachment index from the full message.
Attachment request
curl 'https://mail.zoomfort.com/api/v1/messages/sample-message/attachments/0' \
  -H "Authorization: Bearer $ZOOMFORT_API_KEY" \
  --output notes.txt
Attachment answer · 200 · text/plain
Sample notes.

The example answer is the downloaded file’s content. Expired or purging content returns 410 purged. A missing blob returns 404 blob_unavailable; retry because storage can be eventually consistent.

How mail is checked

For code lookup, zoomfort checks that the mail’s claimed sender agrees with evidence recorded by the receiving mail server. That evidence must say the sender’s domain passed its mail policy, plus either a valid aligned signature or an allowed aligned sending server. These are DMARC, DKIM and SPF respectively. The Worker reads trusted receiver results; it does not independently run those checks.

Evidence comes from the topmost receiver result header: mx.cloudflare.net for Cloudflare ingress or mx.zoomfort.com for the SMTP relay. Lower or mismatched headers do not count. Missing, truncated or ambiguous sender evidence fails the code endpoint’s default verification.

from_domain=service.example matches service.example and notify.service.example, but not lookalikeservice.example or service.example.other.example. It uses the actual From mailbox, not a display name. Case, a trailing dot and equivalent international domain spellings are normalized. URLs, email addresses, wildcards, IP literals, malformed domains and repeated parameters are rejected.

The passing signing or sending domain must equal the actual From domain or be below it. Sibling domains and a parent domain for a subdomain From are rejected even if a mail provider’s relaxed policy accepts them. require_auth accepts only one 0 or 1 value. The stored auth_verdict alone does not prove this stricter code check passed.

FolderWhat goes there
InboxOther mail, and mail a person releases from Quarantine or moves from Digest.
DigestPromotions with classification confidence at least 0.6; or subscribed notifications with an unsubscribe header, importance at most 1 and a request-to-act score below 0.3.
QuarantineFailed sender signals; credential-request or sender-identity-mismatch scores at least 0.7; phishing confidence at least 0.6; or mail a person moves there.

Classification is optional and may arrive after delivery. A failure leaves the category unset while extracted codes and rule-based summaries remain available. Sorting does not delete mail or set the human important star. Quarantine takes precedence; a person can release mail into Inbox. The /code endpoint never returns codes from Quarantine, including with require_auth=0. A key with read:mailbox can still read quarantined codes through a message list with include_quarantined=1, a full message or a compact message. Those reads do not guarantee sender verification.

In message lists (including summaries and snippets) and code-answer subjects, links are limited to ordinary web links on the actual sender’s domain or its subdomains. Other links are removed. This does not follow redirects or establish that a link is safe. Full and compact by-ID content is unchanged by this filtering. The API does not extract verification links.

Statistics

GET /stats requires read:stats and totals messages within the key’s reach.

NameRequiredMeaning
sinceNoInclusive received-at start: ISO timestamp or Unix milliseconds. Defaults to 30 days ago.
toNoInclusive received-at end, in the same formats. Defaults to now; must be at or after since. This is a time, not an address.
Statistics request
curl --get 'https://mail.zoomfort.com/api/v1/stats' \
  -H "Authorization: Bearer $ZOOMFORT_API_KEY" \
  --data-urlencode 'since=2026-10-01T00:00:00Z' \
  --data-urlencode 'to=2026-10-31T23:59:59Z'
Statistics answer · 200
{
  "messages": 10,
  "by_category": {
    "verification_code": 4,
    "promo": 2,
    "unclassified": 4
  },
  "by_folder": {
    "inbox": 8,
    "digest": 2
  },
  "quarantined": 0,
  "codes_extracted": 4,
  "tokens_raw": 1200,
  "tokens_bot": 400,
  "tokens_saved": 800,
  "classified": 6,
  "classified_ratio": 0.6
}
FieldMeaning
messagesTotal visible messages in the range.
by_categoryCounts by category, with unset categories counted as unclassified.
by_folderCounts by folder.
quarantinedCount currently marked quarantined.
codes_extractedCount with a currently retained code.
tokens_rawEstimated tokens for message text and selected headers.
tokens_botEstimated tokens for the defined compact payload, excluding added routing metadata.
tokens_savedSum of each message’s nonnegative raw-minus-compact estimate.
classifiedCount with a category.
classified_ratioClassified count divided by total, from 0 to 1; zero for an empty range.

Deleted messages are excluded. Retained purged stubs still contribute category and historical token totals; their cleared codes no longer count. Token figures are estimates, not tokenizer output or billing savings: roughly one token per four non-CJK characters (rounded up), and one per Han, Hiragana, Katakana or Hangul character.

Errors and rate limits

Errors are JSON with error and message. Code lookup’s no_code and sender_not_verified also carry message_id.

Error example · 404
{
  "error": "no_message",
  "message": "No matching message found."
}
StatusMeaningAgent action
400Invalid or missing parameter.Correct the request; check domain, time, wait, limit and folder values.
401Missing, invalid, revoked or expired key, or inactive account.Check the credential and account with the owner.
403Missing permission, non-account credential for self rotation, or an old rotation secret.Use an appropriately scoped current key.
404Not found, outside the key’s reach, a code lookup outcome, or missing blob.Follow the error code. Other accounts’ objects appear not found.
405Unsupported method.Use GET for reads or POST for self rotation.
409Key changed or expired during rotation.Check which rotation succeeded before retrying.
410Message content has expired or cleanup has begun.The content is unavailable.
429Request limit exceeded.Back off for Retry-After seconds, currently 60.
500Unexpected server error.Retry with backoff; report repeated failures to the owner.
503Search query budget exceeded.Narrow since and retry.
ScopeRequests per 60 seconds
Client IP300, checked before authentication.
Authenticated key120.
Waiting code requests20 per key with wait > 0, in addition to the other limits.

These limits use the configured edge limiters; the local preview uses the same numbers. Search scans allow at most 44 message queries per request. JSON and downloads use Cache-Control: no-store. No browser cross-origin access (CORS) is enabled.

Check liveness

GET /health needs no key or parameters and is exempt from these rate limits. It reports Worker liveness, not database readiness.

Health request
curl 'https://mail.zoomfort.com/api/v1/health' \
  -H "Authorization: Bearer $ZOOMFORT_API_KEY"
Health answer · 200
{
  "ok": true
}

Plans and limits

Free is invite-only. The $4.99 monthly plan is Coming soon. The table distinguishes current service limits from published plan targets.

LimitFree · after approval$4.99 / month · Coming soon
Subdomain1 per account1 per account
Address slots510; extra slots proposed at $0.99 each, one time
Who creates addresses todayYou, in the web inboxYou, in the web inbox
New addresses2 per day5 total during the first 7 days, then 10 per day
Deleted address holds its slot60 days30 days
Unrevoked API keys1050
Largest message, including attachments5 MiB10 MiB
Stored message text via APIAvailable with read:mailboxAvailable with read:mailbox
Ordinary mail retention target7 days30 days (planned per-plan retention)
Important mail count target (not enforced)30; up to 60 with proposed bonuses90; up to 150 with proposed bonuses
Important mail storage target (not enforced)100 MB1 GB

Stored text and HTML are each capped at 900 KiB; the truncated field says when a message was cut.

Today ordinary mail is kept 7 days on every plan; retention by plan is planned. Important mail is kept until you unmark or delete it; the count, storage and bonus targets above are not currently enforced. There is currently no payment or bonus flow.

Address and key limits are enforced. Address slots include retired addresses until their holding period ends; the name itself is never recycled. Operator overrides and assigned extra address slots can change an account’s allowance. Daily address creation uses UTC days; restoration also consumes creation allowance. The first-week paid allowance is a total, not five per day.

Cleanup runs hourly at minute 17. Once ordinary mail is old enough, cleanup removes bodies, codes, headers, summaries and attachment content, then keeps a metadata stub for 90 days after purge. Queue time can delay cleanup beyond the retention window. Mark important before cleanup begins; marking an expired stub cannot restore content.

Important mail does not bypass the incoming message-size limit.

Own domains are planned as a separate higher plan with no price yet. Register interest under Pricing. Every account requires an invite.

Planned

  • Sender binding per address.
  • Agents creating their own addresses by API with the $4.99 plan (Coming soon). The API currently supports address listing only.
  • An MCP server and an SDK.
  • Clean message text in the code request when a service sends a link instead of a code.
  • Sending mail.

No dates yet.