Troubleshooting
The symptoms people actually hit, and what each one turns out to be.
- The API returns 401 and the key looks right
- Check for a newline or a quote in the environment variable: EMAILSSH_API_KEY="esh_..." in a .env file that is parsed loosely can include the quotes. Print the length of the value, do not print the value.
- invalid_from_domain, but the domain is in the dashboard
- Added is not verified. Call POST
/v1/domains/:id/verify and read the records array: each one carries found: true or false, and the false one is your answer. - The domain will not verify and the records are published
- Almost always the DNS host appended the zone to a name that was already absolute. If your host shows _emailssh.mail.acme.com.mail.acme.com, enter just _emailssh. Check with dig TXT _emailssh.mail.acme.com +short.
- The email is queued but never arrives
- Get its status. delivered means it reached the recipient's server and a spam folder is the next place to look. bounced carries the remote server's own words about why.
- Everything lands in spam
- In order: verify the domain including DKIM, send from a subdomain rather than the apex, publish DMARC, put a real text part on every email, and warm up rather than moving all your volume in one day. A brand new domain sending ten thousand messages on day one is the single most common cause.
- It works locally and not in production
- The variable is not in the deployed environment. On Vercel and Netlify a variable added after the last build is not in the running build: redeploy. On Workers it must be a secret, set with wrangler secret put.
- Two of every email
- Something retried. Pass
idempotency_keyderived from what caused the send, and the retry stops being a second email. - 429 from a job that sends a lot
- 600 requests a minute per key, which is 10 a second. Use POST
/v1/emails/batch, which is one request for up to 100 emails, and honour retry-after when it comes. - Webhooks are not arriving
- GET
/v1/webhooks/deliveriesfirst. If there are attempts with a status code, your endpoint is being called and rejecting them. If there are none, check the event is one you subscribed to. - The signature never verifies
- You are almost certainly hashing re-serialised JSON. Sign the raw bytes: in Express that means express.raw({ type: "application/json" }) on that route, not express.json().
- recipient_suppressed on an address that works elsewhere
- It hard bounced or complained on this workspace before. That is a deliberate block. If you know it is fixed, remove it from the suppression list in the dashboard, once.
Getting help
Every response carries an x-request-id. Send it with the email id and the exact time, and support can find the message rather than asking you for more detail: https://emails.sh/contact.