# 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_key derived 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/deliveries first. 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.

---

Base URL: https://emails.sh/v1. Auth: `Authorization: Bearer esh_...`.
Whole API in one file: https://emails.sh/llms.txt. All documentation: https://emails.sh/docs.md.
