Error codes
Every error response has the same shape. docs_url points to the matching entry on this page.
{ "error": { "code": "invalid_template_params", "message": "Invalid template parameters. email: Invalid email address.", "docs_url": "https://docs.frontmail.dev/reference/errors/#invalid-template-params", "details": { "issues": [{ "param": "email", "code": "invalid_email", "message": "Invalid email address." }] } }}429 responses also carry a Retry-After header in seconds. The SDKs throw these errors as typed
FrontmailError subclasses – see Error handling.
bad_request HTTP 400
Meaning: The request could not be understood (wrong content type, unreadable JSON, unsupported method).
How to fix: Send Content-Type: application/json (or multipart/form-data for /v1/send-form) and a valid body. The SDKs do this for you.
invalid_body HTTP 400
Meaning: The JSON body is valid but does not match the schema, e.g. a missing template_id or a wrong field type. details lists the failing fields.
How to fix: Compare the body with the REST API reference and fix the fields listed in details.
invalid_idempotency_key HTTP 400
Meaning: The Idempotency-Key header is empty or longer than 255 characters.
How to fix: Use a UUID or another unique string of at most 255 characters. The SDKs generate one per logical send.
unauthorized HTTP 401
Meaning: No key was provided, or the key could not be matched to an organization.
How to fix: Pass the public key as user_id / X-Frontmail-Public-Key, or the private key as Authorization: Bearer sk_….
invalid_public_key HTTP 401
Meaning: The public key does not exist or was rotated.
How to fix: Copy the current public key from Security → Public key in the dashboard. After a rotation, redeploy your site with the new key.
invalid_private_key HTTP 401
Meaning: The private key is unknown or was revoked.
How to fix: Create a new private key in Security → Private keys and update the secret in your server environment.
insufficient_credits HTTP 402
Meaning: Your organization has no credits left and uses reject mode. In the default hold mode you get 202 with status: "held" instead – unless the hold queue is full (details.reason: "hold_queue_full", with held and limit).
How to fix: Buy a credit pack, enable auto top-up, upgrade, or switch to Hold emails in Security → When you run out of credits. For hold_queue_full, also release or discard held messages. See Hold queue.
forbidden HTTP 403
Meaning: The key or user is not allowed to perform this action.
How to fix: Check the role of the member or use a private key for server-side endpoints.
origin_not_allowed HTTP 403
Meaning: The request came from an Origin that is not in your allowed origins list – or it has no Origin/Referer at all (server, curl, native app) and Allow mobile apps is off. details.reason is no_origin in that case.
How to fix: Add the domain (e.g. example.com or *.example.com) in Security → Allowed websites. Local development needs localhost too. For native apps enable Allow mobile apps; on servers use a private key instead of the public key.
captcha_required HTTP 403
Meaning: The template requires Turnstile and the request carried no token.
How to fix: Render the Turnstile widget (the React/Vue/Svelte FrontmailForm does it for you) or pass turnstile_token. Server-side calls with a private key skip this check.
captcha_failed HTTP 403
Meaning: Cloudflare rejected the Turnstile token – it is invalid, expired (tokens live ~5 minutes) or was already used. With details.reason: "hostname_mismatch" the widget was solved on a different hostname than the one the request came from.
How to fix: Reset the widget and let the user solve it again. Never reuse a token across sends. For hostname_mismatch, add every hostname of your site (e.g. example.com and www.example.com) to your Turnstile widget in Cloudflare. See Turnstile.
captcha_not_configured HTTP 403
Meaning: The template requires Turnstile, but your organization has not added its own Turnstile keys, so requests from websites cannot be verified. In production, Cloudflare test keys (1x…, always pass) are refused as well (details.reason: "test_secret").
How to fix: Create a free Turnstile widget in your Cloudflare account and add its site key and secret key in Security → Bot protection (Turnstile). Or turn off Require CAPTCHA (Turnstile) on the template. See Turnstile.
headless_blocked HTTP 403
Meaning: blockHeadless is on and the request looks like it came from a headless browser.
How to fix: Expected for bots. If a real user is affected, turn off blockHeadless in the SDK init or Block headless browsers in Security → Abuse protection.
recipient_blocked HTTP 403
Meaning: A recipient or the watched variable matches an entry on your block list.
How to fix: Remove the entry from Security → Block list if it was added by mistake.
private_key_required HTTP 403
Meaning: The endpoint (batch, history, templates) or the organization setting requires a private key.
How to fix: Call it from your server with Authorization: Bearer sk_… – never ship a private key to the browser.
private_key_in_browser HTTP 403
Meaning: A private key (Authorization: Bearer sk_… or accessToken) was sent with an Origin header, i.e. from a web page, where every visitor can read it. Private keys are server-side credentials.
How to fix: Use the public key in the browser. Keep the private key on your server. If you really need it in a browser (internal tool), enable Allow private keys in browsers in Security – and rotate the key if it was ever public.
email_not_verified HTTP 403
Meaning: The account owner has not verified their email address yet. Unverified accounts can browse the dashboard but not send.
How to fix: Click the link in the verification email, or resend it from the dashboard banner.
organization_suspended HTTP 403
Meaning: The organization was suspended, usually for abuse or a disputed payment.
How to fix: Contact support@frontmail.dev from the owner address.
plan_limit_reached HTTP 403
Meaning: A plan limit other than credits was hit – number of templates, users, or a feature not on your plan. Items frozen after a downgrade also return this – for sends and for Resend in the dashboard (details.reason: "template_frozen").
How to fix: Upgrade the plan – frozen templates and members are unfrozen automatically – or delete templates / revoke pending invitations to make room. See Plans and Plan changes.
dynamic_recipient_not_allowed HTTP 403
Meaning: A recipient field (To/CC/BCC) is built from a param, but the template does not allow dynamic recipients. This protects you from being used as an open relay.
How to fix: Enable Allow dynamic recipient in the template settings (Settings → Protection) only if you really need it and have Turnstile and allowed origins on.
dynamic_recipient_requires_captcha HTTP 403
Meaning: The template takes its recipient from params and the request uses the public key, but the template does not require CAPTCHA. Without it, anyone who copies your public key could send your template to any address.
How to fix: Add your Cloudflare Turnstile keys under Security → Bot protection and turn on Require CAPTCHA for the template – or send this template from your server with a private key. See Link allowlist & content lock.
dynamic_field_not_allowed HTTP 403
Meaning: A sender field is built from params and the request uses the public key: the From address never may be, and for templates with a dynamic recipient neither may the From name or Reply-To. details.fields lists them.
How to fix: Use fixed values in the template (the visitor’s address belongs in Reply-To only when the recipient is fixed, e.g. a contact form) – or send from your server with a private key.
link_not_allowed HTTP 403
Meaning: The rendered email links to a domain outside your link allowlist. Checked for public-key sends to templates with a dynamic recipient, for auto-replies and for templates with Only allow links to allowed domains. details.hosts lists the offending domains (never the full URL).
How to fix: Add the domain under Security → Allowed link domains, write the link directly in the template, or remove it from the params. See Link allowlist & content lock.
attachments_not_allowed HTTP 403
Meaning: The request contains dynamic attachments, but the template does not allow them, your plan has no attachment allowance (Free), or the file type is blocked (executables, scripts, disk images, HTML/SVG, macro-enabled Office files). POST /v1/uploads with the public key also needs at least one template that allows attachments.
How to fix: Enable Allow attachments from the form in the template settings and make sure your plan includes attachments.
not_found HTTP 404
Meaning: The URL does not match any endpoint.
How to fix: Check the path and the /v1 prefix.
service_not_found HTTP 404
Meaning: The service_id does not exist in the organization of this key, or no default service is set.
How to fix: Copy the ID from Email services, or mark one service as default.
template_not_found HTTP 404
Meaning: The template_id does not exist in the organization of this key.
How to fix: Copy the Template ID from Templates. Keys from a different organization will not see the template.
message_not_found HTTP 404
Meaning: The message does not exist, is older than your history retention, or the status token does not match.
How to fix: Use the message_id and status_token from the send response.
conflict HTTP 409
Meaning: The resource changed concurrently or is in a state that does not allow the operation (e.g. releasing a message that was already sent). With details.reason: "subscription_not_active" a plan change was refused because the subscription has an unpaid invoice.
How to fix: Reload the resource and retry. For subscription_not_active, pay the open invoice (Billing → Plan → Manage cards & invoices) and then change the plan.
idempotency_conflict HTTP 409
Meaning: The same Idempotency-Key was used within 24 hours with a different request body.
How to fix: Generate a new key for every new logical send. Reuse a key only when retrying the identical request.
payload_too_large HTTP 413
Meaning: The request body exceeds the size limit.
How to fix: Move large files to uploads and keep params small.
attachment_too_large HTTP 413
Meaning: The total size of attachments in one request exceeds your plan limit.
How to fix: Compress the files, send a link instead, or upgrade. See Plans.
invalid_template_params HTTP 422
Meaning: The params do not match the template schema – a required param is missing, a value has the wrong type or breaks a rule (length, range, pattern, enum). With strict mode, unknown params also fail. Public-key sends to templates with a dynamic recipient additionally reject HTML params and values over 500 characters (details.reason: "content_locked"). details has one entry per field.
How to fix: Fix the fields listed in details. Generate types with npx frontmail types to catch this at compile time.
template_render_failed HTTP 422
Meaning: Handlebars could not render the template with these params (e.g. #each over a non-list, an unknown helper).
How to fix: Open the template editor – the checks panel shows the problem. Test with the same params in the preview.
invalid_recipient HTTP 422
Meaning: After rendering, a To/CC/BCC/Reply-To value is not a valid email address, or To is empty.
How to fix: Check the recipient fields of the template and the params they use.
sender_not_configured HTTP 422
Meaning: The email has no From address: the template has none and the email service has no default sender (and no connected account or email-like SMTP username to fall back to). Returned by test sends from the dashboard.
How to fix: Set Default From email in the service settings (Email services → service) or a From email in the template settings. With Amazon SES and most APIs the address must be a verified identity.
rate_limited HTTP 429
Meaning: Too many requests for your plan, IP, key, or limitRate setting – or anomaly detection slowed your organization down. details.scope: "recipient" means the recipient already got the daily maximum of dynamic-recipient emails (Security → Emails per recipient per day); details.scope: "uploadbytes" means the daily upload budget for public-key uploads is used up.
How to fix: Wait Retry-After seconds (the SDKs do this automatically). For bulk sends use /v1/send-batch. See Rate limits.
test_send_limit HTTP 429
Meaning: You used all free test sends from the template editor for today (20 per day).
How to fix: Wait until tomorrow (UTC) or send a real message through the API.
recipient_suppressed HTTP 451
Meaning: The recipient is on your suppression list after a hard bounce, a complaint or a manual entry. No credit is used.
How to fix: Only remove the address from Suppressions if you are sure it is valid and the person wants your email.
internal_error HTTP 500
Meaning: Something failed on our side.
How to fix: Retry with the same Idempotency-Key (safe, no duplicates). If it persists, check status.frontmail.dev and contact support with the request ID.
provider_error HTTP 502
Meaning: The email provider returned an unexpected error during a synchronous operation such as a service test.
How to fix: Open the service in the dashboard to see the provider message. See the provider guides.
Client-side (SDK) errors
These codes are produced by the SDKs without an API response. The error has status 0.
network_error SDK
Meaning: The request never got an HTTP response – offline, DNS failure, blocked by an extension or a CORS/CSP rule. Already retried with backoff.
How to fix: Check the connection and your Content-Security-Policy (connect-src https://api.frontmail.dev). Ask the user to try again.
timeout SDK
Meaning: An attempt took longer than timeoutMs (default 15 s) and all retries timed out too.
How to fix: Retrying is safe – the SDK reuses the idempotency key, so no duplicate is sent.
aborted SDK
Meaning: The AbortSignal passed in options.signal was aborted.
How to fix: Nothing to fix – usually the component unmounted or the user navigated away.
invalid_response SDK
Meaning: The server answered with something that is not a Frontmail JSON response – typically a proxy, captive portal or wrong apiUrl.
How to fix: Check the apiUrl option and any proxy between the client and api.frontmail.dev.
not_initialized SDK
Meaning: send() was called before init() and without a key in the call options.
How to fix: Call init({ publicKey }) once at startup, or pass the key as the last argument.
private_key_in_browser SDK
Meaning: A private key (privateKey, or accessToken in @frontmail/emailjs-compat) was configured in code running in a web browser, or @frontmail/node was bundled into a page. Anyone visiting the page could copy the key and read your message history.
How to fix: Use the public key (pk_…) in the browser and keep the private key on your server. Rotate the private key if it was ever deployed to a page. For an internal tool on trusted machines only, pass dangerouslyAllowPrivateKeyInBrowser: true.