Fix contact form 502 or 503 when RESEND_API_KEY is missing in production
I shipped a contact form that passed Turnstile, returned JSON from /api/contact, and still failed to send mail. The browser showed a generic failure and the response body mentioned RESEND_API_KEY or email not being configured.
That is not a Resend domain problem yet. It is an env binding problem: the Worker running your Astro API route cannot see RESEND_API_KEY when the POST handler executes.
503 vs 502: On mayfield.io, a missing key usually returns 503 with Contact form email is not configured. Set RESEND_API_KEY on the server. A 502 with Could not send email (...) means the key was present but Resend rejected the send — see Fix Resend 403 and 422 errors. This post covers the missing-secret path.
What the route checks
In src/pages/api/contact.ts, production refuses to send unless Resend is configured:
if (!isResendConfigured(locals) && !import.meta.env.DEV) {
return Response.json(
{ error: 'Contact form email is not configured. Set RESEND_API_KEY on the server.' },
{ status: 503 },
);
}
isResendConfigured is one line: does readEnv('RESEND_API_KEY') return a non-empty string?
Turnstile can pass first. You can even see the widget on /contact and still hit this error if only the Resend secret is missing. Local dev skips the gate (import.meta.env.DEV), which is why the bug feels sudden after deploy.
Fast diagnosis checklist
1) Read the status code and error text
| Status | Typical body | Meaning |
|---|---|---|
| 503 | Set RESEND_API_KEY on the server | Secret not visible to the route at request time |
| 502 | Could not send email (403) or (422) | Key present; Resend rejected the payload — 403/422 fix post |
| 503 | Set Turnstile keys on the server | Different missing secret — 503 Turnstile post |
If your search started with “502 missing RESEND_API_KEY” but DevTools shows 503, stay on this post. The fix is the same: get the secret onto the Worker.
2) Confirm the secret exists in Cloudflare, not just locally
Production values live in Workers → your Astro service → Settings → Variables and Secrets.
You need:
RESEND_API_KEY— secret (server-only, starts withre_)CONTACT_FROM_EMAIL— plain variable (verified sender domain)CONTACT_TO_EMAIL— plain variable (inbox that receives submissions)
A key in .dev.vars does not exist in production until you create it in the dashboard:
npx wrangler secret put RESEND_API_KEY
Paste the key from the Resend dashboard. Wrangler encrypts it; it will not appear in wrangler.json or git.
3) Verify readEnv can see the binding
If you added the secret in the Cloudflare UI but never redeployed, or typoed the name (RESEND_KEY vs RESEND_API_KEY), readEnv returns undefined and you get 503. See Astro secrets on Cloudflare Workers for how the helper checks cloudflare:workers env first, then Astro getSecret.
Match names exactly to astro.config.mjs env.schema and .dev.vars.example. The contact route does not read import.meta.env.RESEND_API_KEY directly — it goes through readEnv.
4) Production-safe curl probe
After Turnstile is configured, send a probe without a valid token. You want to fail after the Resend gate, not on it:
curl -i -X POST "https://mayfield.io/api/contact" \
-H "Content-Type: application/json" \
-d '{"name":"Probe","email":"you@example.com","message":"Resend config probe from curl — at least ten chars"}'
- Still 503 with Resend message → secret not bound; fix dashboard secrets and redeploy.
- 400 asking for verification → Resend is configured; Turnstile token is the next gate.
- 403 verification failed → Turnstile secret/domain mismatch, not Resend.
Use a real browser submit for the happy path. Do not paste live Turnstile tokens into tickets.
Common mistakes
Tested locally, assumed production. .dev.vars and .env are not deployed. I treat Worker secrets as part of the ship checklist, same as DNS. Local .dev.vars vs production secrets is the file split I copy into every new project.
Set the key as a plain text variable. Resend API keys belong in Secrets, not public Worker variables. Plain vars can leak through logs and dashboard exports you did not intend to share.
Forgot redeploy after wrangler secret put. Secrets attach to the Worker version. I have burned an hour editing Astro code when the binding simply had not reached the live route yet.
Only set RESEND_API_KEY. The route also reads CONTACT_FROM_EMAIL and CONTACT_TO_EMAIL. Missing those defaults to contact@mayfield.io, which is fine if that domain is verified in Resend. If you override them in production, set all three together so you are not debugging a half-configured Worker.
Fix flow that actually sticks
- Open Resend → API Keys → create or copy a production key (
re_...). - Cloudflare dashboard → Workers → your Astro service → Secrets → add
RESEND_API_KEY. - Confirm
CONTACT_FROM_EMAILandCONTACT_TO_EMAILexist as plain variables (or accept the code defaults). - Redeploy:
npm run deployor your CI pipeline tomain. - Re-run the curl probe — expect 400 (Turnstile), not 503 (Resend).
- Submit once from
/contactin a normal browser session. Confirm 200 and inbox delivery.
Once the secret is readable, the same route calls Resend. If mail still fails with 502 and a Resend status in the body, you have moved to a different error class — domain verification, from address, or API key scope — not missing configuration.
For the full stack map (Turnstile + Resend + file layout), see Turnstile + Resend complete setup.
Hero photo: Cat monitor reflection by storem, licensed CC BY-SA 2.0.