AgentMail Documentation Audit
One-line state: a fast-moving, agent-targeted email API whose individual pages read cleanly, but whose cross-page contracts disagree on the most basic things — the base URL, whether API keys can be restricted, what a send request requires, whether there are rate limits at all, and even how to import the SDK — exactly the silent failures that break the AI agents these docs are built for.
1. Base URL contradicts itself — /v0/ is mandatory on one page, absent from the Quickstart examples (critical)
Location: https://docs.agentmail.to/api-reference.mdx vs https://docs.agentmail.to/quickstart.mdx
Problem: The API Welcome page states plainly: "All API requests should be made to the following base URL: https://api.agentmail.to/v0/". But per the scraped Quickstart, every cURL example targets https://api.agentmail.to/inboxes... and /agent/sign-up with no /v0/ segment. The canonical Send Message reference uses the versioned form (POST https://api.agentmail.to/v0/inboxes/{inbox_id}/messages/send), and the 403 KB article references /v0/inboxes//messages. So the version segment is present on the reference pages and absent on the page a beginner copies first.
Consequence: A developer or an AI agent that copies the Quickstart cURL verbatim hits an un-versioned path. Either it 404s, or — worse for debugging — it silently resolves to different behavior than the versioned reference describes. An agent has no way to reconcile two equally authoritative pages and will fail without a useful error.
The fix: Pick one canonical base URL (https://api.agentmail.to/v0/) and make every cURL, SDK, and KB example use it. Add a CI check that greps docs for api.agentmail.to/inboxes without /v0/.
2. The 403 troubleshooting guide flatly denies the existence of the Permissions feature (critical)
Location: https://docs.agentmail.to/knowledge-base/api-403-error.mdx vs https://docs.agentmail.to/permissions.mdx
Problem: The Permissions page documents a deny-by-default whitelist on API keys: "When you provide a permissions object, it acts as a whitelist: only the permissions you explicitly set to true are granted. Everything else is denied," covering message_send, inbox_create, and label-visibility permissions. The 403 KB article says the opposite: "AgentMail does not place any feature restrictions on your API key. Every API key has access to all endpoints. If you are getting a 403, it is not because your key lacks permissions." The Introduction page also advertises "Built-in Security: Use API permissions and agent guardrails to control access." Three surfaces affirm permission scoping; the one page a developer reads when debugging a 403 denies it.
Consequence: The single most likely cause of a 403 on a restricted key — a permission the key wasn't granted — is the one cause the troubleshooting guide explicitly tells you to rule out. A developer (or an agent reasoning over the KB) will chase suppression lists and ownership errors while the real problem is a whitelist they configured. This sends debugging in exactly the wrong direction.
The fix: Rewrite the 403 article to list "API key permissions deny this operation" as the first cause, link to the Permissions page, and delete the "We do not restrict your API key / Every API key has access to all endpoints" section.
3. The canonical Send Message example sends an empty body, the schema requires nothing, and the migration guide calls it a drop-in (critical)
Location: https://docs.agentmail.to/api-reference/inboxes/messages/send.mdx and https://docs.agentmail.to/knowledge-base/what-is-agentmail.mdx
Problem: The SendMessageRequest schema lists to, subject, text, html, etc. as properties but declares no required: list — every field is optional. Accordingly, the generated SDK examples send an empty payload: the Node example is await client.inboxes.messages.send("inbox_id", {}) and the Python example is client.inboxes.messages.send(inbox_id="inbox_id") with no message at all; the Go/Ruby/Java/PHP/C#/Swift examples all POST "{}" with an empty Authorization: Bearer header. Meanwhile the migration KB promises this is a near drop-in: "Replace your existing send call with client.inboxes.messages.send() and you get the same sending capability."
Consequence: The headline example of an email API teaches nothing — it never shows a recipient, subject, or body — and if it represents the real contract, a developer can issue an empty send that errors at runtime or dispatches a blank email. A team migrating on the strength of "just replace your send call" has no runnable example to copy. An AI agent extracting "how to send a message" learns the wrong shape entirely.
The fix: Mark to (and realistically subject plus one of text/html) as required in the OpenAPI spec so generated examples populate them, and replace the {} payloads with a complete, runnable example including recipient, subject, and body — the same example the migration guide should link to.
4. Quickstart paths are broken — the inbox_id placeholder is missing (critical)
Location: https://docs.agentmail.to/quickstart.mdx
Problem: Per the scraped Quickstart, the cURL/CLI examples omit the <inbox_id> path segment entirely, producing requests like inboxes//messages/send (double slash) and --inbox-id flags with no value. The 403 KB article documents the resulting symptom directly: "calling /v0/inboxes//messages with an empty inbox_id may return 403 instead of 400."
Consequence: A developer copying the Quickstart sends requests to a malformed path and receives a 403 — and per the company's own KB, a misleading 403 rather than a clear 400. The first end-to-end flow a new user runs fails, and the error code points away from the real cause (a missing path parameter).
The fix: Insert a clearly-marked placeholder (e.g. inboxes/{inbox_id}/messages/send) in every Quickstart example, and have the prior step show how to capture the inbox id into a shell variable so the examples are runnable in sequence.
5. A single TypeScript example reads the inbox id two different ways (significant)
Location: https://docs.agentmail.to/inboxes.mdx (cross-referenced with quickstart.mdx)
Problem: Within one TypeScript example on the Inboxes page, the inbox id is read as newInbox.id in one place and retrievedInbox.inbox_id in another — at most one of these is the real JS/TS field. (The Python block's new_inbox.inbox_id and the Quickstart's inbox.inboxId differ from each other by language convention — snake_case vs camelCase — which is normal and not itself a defect; the breaking inconsistency is the id vs inbox_id mismatch inside the same TS sample.)
Consequence: A developer who copies the Inboxes TS example gets undefined for the inbox id from whichever access path is wrong. That undefined then flows into the request path and produces the broken inboxes/undefined/messages/send request — the same malformed-path 403 from Finding 4. Because property access on the wrong field doesn't throw, the bug surfaces far from its cause.
The fix: Standardize on the SDK's actual TS property name (verify against the generated types) and make both lines of the Inboxes TS sample use it. Add a compile step that runs the doc snippets against the real SDK types.
6. The SDK is imported two incompatible ways — default export vs named export (significant)
Location: https://docs.agentmail.to/webhook-verification.mdx vs the Quickstart / Inboxes / API-reference pages and the homepage hero
Problem: The webhook-verification page imports the SDK as a default export with a differently-named class: import AgentMail from "agentmail"; const client = new AgentMail();. Every other TypeScript surface uses a named import with a different class: import { AgentMailClient } from "agentmail"; const client = new AgentMailClient({ apiKey: ... }). The homepage hero shows a third shape in Python: from agentmail import AgentMail; client = AgentMail(). The two JS forms are mutually exclusive — one of them does not exist in the package.
Consequence: A developer following the webhook guide gets a module/constructor error (AgentMail is not a constructor, or an undefined default import). An AI agent that synthesizes from both pages can't tell which export is real and will emit non-compiling code.
The fix: Decide the public JS export surface (e.g. named AgentMailClient) and fix the webhook-verification example to match. If the Python (AgentMail) and JS (AgentMailClient) names are intentionally different, state that explicitly so the asymmetry isn't read as an error.
7. The Introduction promises "no restrictive rate or sending limits" — the docs then document exactly those limits (significant)
Location: https://docs.agentmail.to/introduction.mdx vs https://docs.agentmail.to/knowledge-base/rate-limits.mdx and https://agentmail.to/pricing
Problem: The Introduction sells the platform on "High-Volume Ready: No restrictive rate or sending limits," and frames legacy providers' "restrictive rate and sending limits" as the problem AgentMail solves. But the rate-limits article states "All API endpoints are rate-limited per API key" and caps sending at 3,000 emails/month on Free, and the pricing page enforces a hard 100 emails/day on Free. There are rate limits and sending limits; the headline feature claim says there are none.
Consequence: A developer choosing AgentMail because the Introduction promised no sending limits builds against that assumption and is then blindsided by a 429 and by per-plan monthly/daily ceilings. The marketing claim and the operational reference contradict each other on a load-bearing decision (will my high-volume agent get throttled?).
The fix: Replace "No restrictive rate or sending limits" with an accurate framing — e.g. "high per-key limits with generous plans" — and link directly to the rate-limits table so the selling point and the contract agree.
8. No numeric API request-rate limit is documented anywhere (significant)
Location: https://docs.agentmail.to/knowledge-base/rate-limits.mdx
Problem: The rate-limits article gives monthly volume and inbox counts per plan, then says only: "All API endpoints are rate-limited per API key. If you exceed the limit, the API returns a 429 Too Many Requests response with a Retry-After header." There is no requests-per-second/minute figure anywhere, and the "Tips for high-volume agents" section offers "Implement exponential backoff" with no ceiling to back off toward.
Consequence: Developers building high-volume agents — the stated target audience — cannot size concurrency, backoff, or batching without a number. Agents that auto-retry on 429 without a documented ceiling hammer the API blindly until they're throttled.
The fix: Publish the actual per-key request-rate limit (and any burst allowance) in a parseable table, alongside the existing monthly-volume table.
9. Pricing enforces a Free-plan 100 emails/day cap and a webhook-endpoint cap the docs never mention (significant)
Location: https://agentmail.to/pricing vs https://docs.agentmail.to/knowledge-base/rate-limits.mdx
Problem: The pricing table includes an "Emails/day" row (Free = 100/day, others "No limit") and a "Webhook Endpoints" cap (Free/Developer = 2). The docs' rate-limits article lists only monthly limits (Free = 3,000/month) and never mentions a daily ceiling or a webhook-endpoint cap.
Consequence: A Free-plan developer who reads only the docs budgets against 3,000/month and is blindsided when sends stop at 100 in a day. An agent provisioning webhooks against the docs can try to create a third endpoint and fail against a cap that isn't in the rate-limits reference.
The fix: Mirror the pricing table's daily-send and webhook-endpoint caps into the rate-limits article, and treat pricing-page limits and docs limits as a single source so they can't drift.
10. The idempotency page never warns that messages.send is excluded — the carve-out lives only in a separate KB article (significant)
Location: https://docs.agentmail.to/idempotency.mdx vs https://docs.agentmail.to/knowledge-base/preventing-duplicate-sends.mdx
Problem: The Idempotency guide states broadly: "AgentMail supports idempotency for all create operations via an optional client_id," and describes a repeated client_id as a safe no-op that "immediately return[s] a 200 OK response with the data from the original" request. It never states that messages.send is not covered. That exclusion appears only in a separate KB article: "The clientId parameter is for resource creation, not for messages.send… To prevent duplicate email sends, you need to handle this in your application logic." (The rate-limits page agrees with the KB, so the two are internally consistent — the gap is that the idempotency page itself gives no signal that sends are unprotected.)
Consequence: A developer reading only the idempotency page sees "idempotency for all create operations" and reasonably retries a failed messages.send with the same client_id, expecting deduplication. Sends aren't deduplicated, so the customer receives a duplicate email — and this is the single operation developers most need protected against retries.
The fix: On the idempotency page, explicitly carve out messages.send ("client_id does not deduplicate sends — see Preventing Duplicate Sends") and link to the drafts-based deduplication workflow wherever sending is described.
11. Metrics API was renamed in the changelog but the API reference still shows only the old endpoint (significant)
Location: https://docs.agentmail.to/changelog vs https://docs.agentmail.to/llms.txt (API reference index)
Problem: The June 12, 2026 changelog states GET /v0/metrics "is replaced by /metrics/events," adds GET /v0/metrics/usage (plus pod/inbox variants), and replaces client.metrics.query() with client.metrics.queryEvents() — noting the old path is "removed from the docs and SDKs." But the reference index in llms.txt still lists only "Query Metrics" pages (metrics/query.mdx, plus pod/inbox variants), with no metrics/events or metrics/usage entries.
Consequence: The reference documents a method (client.metrics.query()) the changelog says was removed, and omits the new usage/events endpoints entirely. A developer or agent reading the reference uses a deprecated call and never discovers the new usage-totals feature.
The fix: Regenerate the API reference from the current OpenAPI spec so metrics/events and metrics/usage (org/pod/inbox) appear and the bare metrics/query page reflects its deprecated status.
12. Webhook sub-events documented in the guide are absent from the API reference (significant)
Location: https://docs.agentmail.to/events.mdx vs https://docs.agentmail.to/llms.txt (Webhooks > Events index)
Problem: The Webhook Events guide documents four message.received* variants — message.received, message.received.spam, message.received.blocked, message.received.unauthenticated — and adds a critical caveat: "message.received.unauthenticated must be explicitly included in event_types," while .spam/.blocked require label_spam_read/label_blocked_read permissions. The reference Events index in llms.txt lists only seven events with a single "Message Received" — no .spam, .blocked, or .unauthenticated, and no mention of the explicit-subscription requirement.
Consequence: A developer wiring webhooks from the API reference never subscribes to message.received.unauthenticated (which won't arrive unless explicitly requested) and doesn't know the spam/blocked variants exist or are permission-gated. Inbound emails that should be handled silently never trigger a webhook.
The fix: Add reference pages for each message.received.* sub-event, document the explicit event_types requirement and the permission gates in the reference itself, and don't leave that contract only in the prose guide.
13. Four alternate API servers are exposed in the spec but never documented (significant)
Location: https://docs.agentmail.to/api-reference/inboxes/messages/send.mdx
Problem: The Send Message reference's servers: block lists four hosts — https://api.agentmail.to, https://x402.api.agentmail.to, https://mpp.api.agentmail.to, and https://api.agentmail.eu — none of which are explained anywhere in the docs. The API Welcome page names only the single canonical base URL, and the EU region is mentioned only as an Enterprise pricing perk.
Consequence: Tooling that consumes the OpenAPI spec (including AI agents auto-selecting servers[0] or enumerating options) may send requests to the x402., mpp., or .eu hosts with no idea what they mean, what auth they expect, or whether they're production. EU-data-residency users have no documented path to the .eu host.
The fix: Either remove non-public servers from the published spec, or document each host (purpose, who should use it, region/data-residency implications) and mark the default explicitly.
14. The changelog is invisible on the markdown surface the docs tell agents to use (significant)
Location: https://docs.agentmail.to/changelog and https://docs.agentmail.to/llms.txt
Problem: llms.txt instructs AI agents: "For clean Markdown of any page, append .md to the page URL." But for the changelog, the markdown route returns only the title — the dated entries (including the June 12 metrics rename) render only in the JS-rendered HTML page. So the markdown surface the docs advertise to agents omits the changelog body.
Consequence: An AI agent following the documented .md convention to learn "what changed recently" gets an effectively empty page and never sees the metrics-API rename or other breaking changes — the exact information that prevents it from emitting deprecated calls (see Finding 11).
The fix: Ensure the changelog renders fully at its markdown route like every other page, or list the latest entries in llms.txt itself.
15. The to field's type is never shown concretely — the only example is an array, the schema is untyped (minor)
Location: https://docs.agentmail.to/api-reference/inboxes/messages/send.mdx and https://docs.agentmail.to/knowledge-base/preventing-duplicate-sends.mdx
Problem: The Send Message schema lists to as a property with no visible type and no example value (and no required list — see Finding 3), so the reference never shows what to accepts. The only concrete recipient value anywhere in the docs is the array form in the drafts example: to: ["customer@example.com"]. Whether to also accepts a bare string is undocumented.
Consequence: A developer can't tell from the reference whether to takes a string, an array, or both, and the lone array example may not generalize to messages.send. Passing the wrong shape risks a validation error or a silently dropped recipient. Agents generating code will guess inconsistently.
The fix: Give to an explicit type and example in the schema (e.g. string | string[]) and use that one shape consistently in every send and draft example.
16. Official docs carry an unprofessional, imprecise tone in load-bearing copy (minor)
Location: https://docs.agentmail.to/introduction.mdx
Problem: The Introduction page — the first doc a new developer reads — includes "The Gmail API sucks!" and the parenthetical "($12/inbox/month !!)". These sit alongside the substantive feature claims rather than in a clearly informal aside.
Consequence: Beyond polish, the editorializing dilutes the credibility of the comparison and gives an AI agent ingesting the page noise instead of a verifiable claim (no source for the "$12/inbox/month" figure, no neutral statement of the limitation). It reads as marketing venting in a reference context.
The fix: Replace the editorializing with a neutral, sourced statement of the limitation (e.g. "per-inbox monthly pricing on legacy providers is costly at agent scale") and reserve opinionated framing for a blog post.
What they do well
- Strong machine-readable foundation: a published
llms.txt, OpenAPI and AsyncAPI specs, per-page markdown output, and a documented MCP server — the right scaffolding for the agent audience. - Idempotency, permissions, and webhook-verification each get dedicated, conceptually clear pages (the recurring problem is cross-page consistency, not per-page quality).
- Plan/limit tables exist on both pricing and the rate-limits article — the data is there; it just needs to be reconciled into one source.
Top 3 recommendations
- Fix the contradictions that break agents silently: unify the base URL on
/v0/(Finding 1), rewrite the 403 guide to acknowledge permissions (Finding 2), and ship a single send example with required fields and a populated body (Finding 3). - Make doc code actually run: standardize the JS SDK import/client name and the TS inbox-id property across every snippet, and restore the missing
inbox_idplaceholders (Findings 4, 5, 6). - Tell one story about limits, and regenerate the reference from the live spec: reconcile the "no limits" claim with the documented monthly/daily/webhook caps and publish a numeric request-rate limit (Findings 7–9), and surface the renamed metrics endpoints, the
message.received.*sub-events, and the alternate servers (Findings 11–13).