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
- Sign in to your inbox and create an address, such as
signup@yourname.zoomfort.com. - Under Settings, API keys, create a key with
read:codes. Addread:mailboxif the agent needs to read messages. Choose which addresses it can read. Creating a key asks for your current password again. - 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.
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": "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.
curl --get 'https://mail.zoomfort.com/api/v1/addresses' \
-H "Authorization: Bearer $ZOOMFORT_API_KEY"{
"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.
| Permission | Allows |
|---|---|
read:codes | Get a verification code and wait for one. |
read:mailbox | List messages, read full or compact messages, and download attachments. |
read:stats | Read 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.
curl -X POST 'https://mail.zoomfort.com/api/v1/keys/self/rotate' \
-H "Authorization: Bearer $ZOOMFORT_API_KEY"{
"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.
| Name | Required | Meaning |
|---|---|---|
to | Yes | An address within the key’s reach, such as signup@yourname.zoomfort.com. |
from_domain | No; recommended | Expected sender domain. Matches that domain and its subdomains; see sender checks. |
since | No; recommended | Inclusive received-at lower bound. ISO timestamp or Unix milliseconds; defaults to 0 (all retained mail). |
wait | No | Seconds to wait; defaults to 0. Nonnegative numbers are capped at 25. |
require_auth | No | Defaults to 1: require a verified sender. 0 skips verification, but keeps sender-domain, quarantine and retention filters. |
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": "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.
| Answer | Meaning | Agent action |
|---|---|---|
no_message | No 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_code | Matching mail has no extracted code. The answer includes message_id. | Read that message; a service may have sent a link. |
sender_not_verified | Matching 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.
| Name | Required | Meaning |
|---|---|---|
to | No | Limit to an address within the key’s reach; otherwise search all reachable addresses. |
since | No | Inclusive received-at lower bound: ISO timestamp or Unix milliseconds. Defaults to 0. |
limit | No | Positive integer; defaults to 50 and is capped at 100 after filtering. |
folder | No | inbox, digest or quarantine. Quarantine also needs include_quarantined=1. |
include_quarantined | No | Set to 1 to include Quarantine; otherwise quarantined messages are excluded. |
from_domain | No | Expected sender domain or its subdomains, as for code lookup. |
require_auth | No | 0 by default; set 1 to require verified senders. |
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'{
"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.
| Name | Required | Meaning |
|---|---|---|
id (path) | Yes | Message ID from the list or code answer, within the key’s reach. |
curl --get 'https://mail.zoomfort.com/api/v1/messages/sample-message' \
-H "Authorization: Bearer $ZOOMFORT_API_KEY"{
"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.
| Name | Required | Meaning |
|---|---|---|
id (path) | Yes | A reachable message ID. |
view | Yes for compact | Set to compact; otherwise the full message is returned. |
curl --get 'https://mail.zoomfort.com/api/v1/messages/sample-message?view=compact' \
-H "Authorization: Bearer $ZOOMFORT_API_KEY"{
"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.
| Name | Required | Meaning |
|---|---|---|
id (path) | Yes | A reachable message ID. |
idx (path) | Yes | Nonnegative attachment index from the full message. |
curl 'https://mail.zoomfort.com/api/v1/messages/sample-message/attachments/0' \
-H "Authorization: Bearer $ZOOMFORT_API_KEY" \
--output notes.txtSample 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.
| Folder | What goes there |
|---|---|
| Inbox | Other mail, and mail a person releases from Quarantine or moves from Digest. |
| Digest | Promotions 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. |
| Quarantine | Failed 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.
| Name | Required | Meaning |
|---|---|---|
since | No | Inclusive received-at start: ISO timestamp or Unix milliseconds. Defaults to 30 days ago. |
to | No | Inclusive received-at end, in the same formats. Defaults to now; must be at or after since. This is a time, not an address. |
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'{
"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
}| Field | Meaning |
|---|---|
messages | Total visible messages in the range. |
by_category | Counts by category, with unset categories counted as unclassified. |
by_folder | Counts by folder. |
quarantined | Count currently marked quarantined. |
codes_extracted | Count with a currently retained code. |
tokens_raw | Estimated tokens for message text and selected headers. |
tokens_bot | Estimated tokens for the defined compact payload, excluding added routing metadata. |
tokens_saved | Sum of each message’s nonnegative raw-minus-compact estimate. |
classified | Count with a category. |
classified_ratio | Classified 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": "no_message",
"message": "No matching message found."
}| Status | Meaning | Agent action |
|---|---|---|
| 400 | Invalid or missing parameter. | Correct the request; check domain, time, wait, limit and folder values. |
| 401 | Missing, invalid, revoked or expired key, or inactive account. | Check the credential and account with the owner. |
| 403 | Missing permission, non-account credential for self rotation, or an old rotation secret. | Use an appropriately scoped current key. |
| 404 | Not found, outside the key’s reach, a code lookup outcome, or missing blob. | Follow the error code. Other accounts’ objects appear not found. |
| 405 | Unsupported method. | Use GET for reads or POST for self rotation. |
| 409 | Key changed or expired during rotation. | Check which rotation succeeded before retrying. |
| 410 | Message content has expired or cleanup has begun. | The content is unavailable. |
| 429 | Request limit exceeded. | Back off for Retry-After seconds, currently 60. |
| 500 | Unexpected server error. | Retry with backoff; report repeated failures to the owner. |
| 503 | Search query budget exceeded. | Narrow since and retry. |
| Scope | Requests per 60 seconds |
|---|---|
| Client IP | 300, checked before authentication. |
| Authenticated key | 120. |
| Waiting code requests | 20 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.
curl 'https://mail.zoomfort.com/api/v1/health' \
-H "Authorization: Bearer $ZOOMFORT_API_KEY"{
"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.
| Limit | Free · after approval | $4.99 / month · Coming soon |
|---|---|---|
| Subdomain | 1 per account | 1 per account |
| Address slots | 5 | 10; extra slots proposed at $0.99 each, one time |
| Who creates addresses today | You, in the web inbox | You, in the web inbox |
| New addresses | 2 per day | 5 total during the first 7 days, then 10 per day |
| Deleted address holds its slot | 60 days | 30 days |
| Unrevoked API keys | 10 | 50 |
| Largest message, including attachments | 5 MiB | 10 MiB |
| Stored message text via API | Available with read:mailbox | Available with read:mailbox |
| Ordinary mail retention target | 7 days | 30 days (planned per-plan retention) |
| Important mail count target (not enforced) | 30; up to 60 with proposed bonuses | 90; up to 150 with proposed bonuses |
| Important mail storage target (not enforced) | 100 MB | 1 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.