Resend Documentation Audit
Resend's docs are broad, current, and unusually agent-aware (llms.txt, llms-full.txt, per-page .md exports, an MCP server, AI onboarding pages) — but the flagship AI export is structurally broken, the error reference contradicts itself and the API introduction, and two webhook code examples fail as written.
1. llms-full.txt strips every API parameter name (critical)
Location: https://resend.com/docs/llms-full.txt (recommended from https://resend.com/docs/introduction: "To view the full text of the documentation, see llms-full.txt")
Problem: In the 2.17 MB full-text export, every API reference parameter component has lost its name attribute. The file contains 527 <ParamField> and 109 <ResendParamField> occurrences, and exactly zero carry a body= or path= attribute (grep -c 'ParamField body=' llms-full.txt → 0). The Send Email section reads:
<ParamField type="string">
Sender email address.
</ParamField>
<ParamField type="string | string[]">
Recipient email address. For multiple addresses, send as an array of strings.
Max 50.
</ParamField>
— descriptions with no from, no to, no subject. The per-page export (/docs/api-reference/emails/send-email.md) proves the source is fine: there, the same blocks read <ParamField body="from" type="string" required>. The stripping is a defect in how llms-full.txt is generated.
Consequence: The one file Resend explicitly tells AI agents to ingest documents the entire API surface — every endpoint's body, path, and query parameters — without a single parameter name. An agent that builds requests from llms-full.txt must guess field names or hallucinate them; required-vs-optional flags are also lost. This silently degrades exactly the workflow (Claude Code, Cursor, MCP-adjacent agents) that Resend's "AI onboarding" strategy targets.
The fix: Fix the llms-full.txt generator to preserve body/path/query and required attributes on ParamField components (or render them as plain markdown like **from** (string, required)), and add a CI check that fails the export when a ParamField appears without a name.
2. Error reference contradicts itself and the API introduction on API-key failures (significant)
Location: https://resend.com/docs/api-reference/errors and https://resend.com/docs/api-reference/introduction
Problem: Three inconsistencies on the page whose subtitle promises a "comprehensive breakdown of all error codes":
restricted_api_keyis listed twice with different statuses and meanings — once as 401 "This API key is restricted to only send emails" and once as 403 "API key is not active".- The API introduction's response-code table says
403= "The API key used was invalid", but no invalid-API-key error type exists anywhere on the errors page (the closest arerestricted_api_keyandsuspended_api_key). - The API introduction references "a
403error with error code1010" for missing User-Agent headers, but code 1010 is absent from the errors page — it is documented only in a knowledge-base article (/docs/knowledge-base/403-error-1010), which itself explains "The error doesn't come from the Resend API itself".
Consequence: A developer (or agent) writing error-handling logic cannot map status codes to error types deterministically: catching restricted_api_key requires handling two different statuses with two different remediations, a 403 from an invalid key has no documented type to match on, and the 1010 case — explicitly called out as confusing by Resend's own KB — is invisible to anyone who trusts the "comprehensive" reference.
The fix: Give each error type a unique name (e.g. split into restricted_api_key and inactive_api_key), add the invalid-API-key 403 case and edge-layer code 1010 to the errors page, and reconcile the introduction's status table with the error list.
3. Webhook verification example reads headers in a way that always returns undefined (significant)
Location: https://resend.com/docs/webhooks/verify-webhooks-requests
Problem: The primary SDK verification example is a Next.js App Router handler (export async function POST(req: NextRequest) with await req.text() and NextResponse), but it reads the Svix headers with bracket indexing:
headers: {
id: req.headers['svix-id'],
timestamp: req.headers['svix-timestamp'],
signature: req.headers['svix-signature'],
},
On a NextRequest, req.headers is a Fetch-API Headers object — bracket access returns undefined; the correct call is req.headers.get('svix-id'). The snippet is also fenced as ```js while containing TypeScript annotations, and the following prose misspells "libaries".
Consequence: Anyone who copies the canonical verification example gets undefined for all three signature headers, so every webhook — legitimate or forged — fails verification with the generic 400. Because this is the security-critical snippet, developers debugging it may be tempted to skip verification entirely rather than find the Headers-API subtlety.
The fix: Use req.headers.get('svix-id') (etc.) in the App Router example, label the fence ts, and add a second variant for pages-API/Express-style req.headers['svix-id'] if that pattern is intended.
4. Webhook quickstart endpoint never sends the 200 it tells you to send (significant)
Location: https://resend.com/docs/webhooks/introduction ("Create a dev endpoint to receive requests")
Problem: The example handler is:
export default (req, res) => {
if (req.method === 'POST') {
const event = req.body;
console.log(event);
res.status(200);
}
};
immediately followed by: "On receiving an event, respond with an HTTP 200 OK to signal to Resend that the event was successfully delivered." In a Next.js pages-API/Express-style handler, res.status(200) only sets the status — it never sends the response (that requires .end(), .send(), or .json()), and non-POST requests fall through with no response at all.
Consequence: The copied endpoint hangs every delivery until timeout. Resend's retry machinery then treats every event as failed, so the first thing a developer sees when following the webhooks quickstart is a wall of retries for a handler that "worked" locally (the console.log fires).
The fix: Change the snippet to res.status(200).end() (or .json({received: true})) and return a 405 for non-POST methods.
5. The 30-day scheduling limit is absent from the API reference that documents scheduled_at (significant)
Location: https://resend.com/docs/api-reference/emails/send-email.md (scheduled_at parameter) vs https://resend.com/docs/dashboard/emails/schedule-email
Problem: The Send Email API reference documents scheduled_at as: "Schedule email to be sent later. The date should be in natural language (e.g.: in 1 min) or ISO 8601 format… [See examples]" — with no maximum window. The constraint "Emails can be scheduled up to 30 days in advance" appears only on the dashboard guide and inside SDK guide accordions.
Consequence: A developer (or agent) working from the API reference — the canonical contract for the parameter — will schedule 31+ days out (e.g. renewal reminders, annual notices) and hit a validation failure the reference gave no reason to expect. Agents consuming only the reference page cannot discover the cap at all.
The fix: Add "Maximum: 30 days in advance" directly to the scheduled_at parameter description on both the send-email and send-batch-emails reference pages.
6. Published .md exports mix three link conventions, and their root-relative links 404 (minor)
Location: https://resend.com/docs/introduction.md (representative; pattern appears across .md exports)
Problem: One page's exported markdown contains href="/send-with-nodejs", href="send-with-express/", href="dashboard/emails", and href="/docs/webhooks/introduction" — three different conventions. The rendered site normalizes them, but the .md files are the artifacts Resend serves to agents and markdown readers, where root-relative links resolve against resend.com and 404 (verified: https://resend.com/send-with-nodejs → 404; the real page is /docs/send-with-nodejs). The /docs/-prefixed style would 404 if resolved against the docs base path (verified: https://resend.com/docs/docs/webhooks/introduction → 404).
Consequence: Any consumer that follows links inside the published markdown — an agent crawling from llms.txt, or a developer reading the .md directly — hits 404s or has to guess which of three base paths each link intends.
The fix: Normalize all links in the .md exports to fully-qualified https://resend.com/docs/... URLs at export time.
7. Node.js quickstart's embedded AI prompt contradicts its own prerequisite (minor)
Location: https://resend.com/docs/send-with-nodejs (copyable "Prompt" block)
Problem: The AI guardrail prompt states "The API key must be stored in an environment variable called RESEND_API_KEY" and then immediately demonstrates initialization with a hardcoded literal: const resend = new Resend('YOUR_RESEND_API_KEY'); — with no process.env.RESEND_API_KEY variant anywhere in the block.
Consequence: This block exists specifically to be pasted into AI agents verbatim ("All AI-generated advice or code related to sending email with Resend must follow these guardrails"). An agent following it faithfully receives two mutually exclusive rules and will commonly emit the hardcoded-string version — the exact secret-handling mistake the env-var rule tries to prevent.
The fix: Make the code match the rule: const resend = new Resend(process.env.RESEND_API_KEY);.
8. Batch sending reference example targets a real third-party mailbox (minor)
Location: https://resend.com/docs/api-reference/emails/send-batch-emails
Problem: The request example sends to foo@gmail.com, while every other quoted example in the docs uses the safe test address delivered@resend.dev (e.g. schedule-email, send-with-nodejs, templates).
Consequence: Copy-pasting the batch example on a verified domain delivers real email to a stranger's Gmail address — and inconsistent example addresses train agents to treat arbitrary real addresses as acceptable placeholders.
The fix: Use delivered@resend.dev in the batch examples, consistent with the rest of the docs.
What they do well
- Genuinely agent-first distribution: llms.txt (360 pages, zero dead links in a 15-link sample), per-page .md exports, a hosted MCP server, and copyable AI prompt blocks.
- Limits are documented as contracts: exact rate-limit headers (IETF draft), quota headers, per-team scoping, no-burst semantics, and a 4% bounce-rate threshold with consequences.
- Error entries include suggested remediations and links to dedicated troubleshooting pages (e.g. domain-mismatch, resend.dev sandbox restrictions).
Top 3 recommendations
- Fix the llms-full.txt generator so ParamField names and
requiredflags survive the export, and gate it in CI. - Reconcile the error reference with the API introduction: unique error-type names, one status per type, and include the edge-layer 1010 case.
- Run the webhook examples as written before publishing — both the verification handler and the quickstart endpoint currently fail on copy-paste.