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.