Email setup so easy your assistant does it for you.

Password resets, receipts and alerts, from your own domain, landing in the inbox.

Paste this at your coding assistant

Add emails.sh to this project. Read https://emails.sh/llms.txt
Create an account

3,000 emails a month, free, no credit card.

Clients for ten languages

Every sample is a complete file. Copy it, add your key, and it runs.

send.js, Node 18 or newer
// npm install @emails.sh/sdk
// Your key is on https://emails.sh/dashboard/keys and starts with esh_
import { Emailssh } from '@emails.sh/sdk';

const mail = new Emailssh({ apiKey: process.env.EMAILSSH_API_KEY });

async function sendLoginCode(to, code) {
  const sent = await mail.send({
    from: 'Acme <hello@acme.com>',
    to,
    subject: 'Your login code',
    html: `<p>Your code is <strong>${code}</strong>.</p>`,
    text: `Your code is ${code}.`
  });

  return sent.id;
}

try {
  const id = await sendLoginCode('someone@example.com', '123456');
  console.log('queued as', id);
} catch (error) {
  // Refusals come back as prose that says what to do next, so this is
  // worth logging verbatim rather than reducing to a status code.
  console.error(error.message);
}

Move from Resend, Postmark, SendGrid, or Mailgun

We answer their APIs exactly as they do, so you keep the package you already installed.

.env, a Node app moving off Resend
  # .env
-RESEND_BASE_URL=https://api.resend.com
-RESEND_API_KEY=re_your_old_key
+RESEND_BASE_URL=https://emails.sh/resend
+RESEND_API_KEY=esh_your_new_key
  
  # Nothing else changes. Same package, same import, same call,
  # same id field on the way back. Deleting these two rolls it back.

Errors that tell you what to do

Every failure comes back with the fix written out, not just a code to look up.

Every error, and what to do about it
The response, in full
HTTP/1.1 422 Unprocessable Entity
content-type: application/json

{
  "error": "invalid_from_domain",
  "message": "The domain acme.com is not verified on this workspace. Verify it at https://emails.sh/dashboard/domains, or send from onboarding@emails.sh while you are testing."
}

The rest of what
sending needs.

On every plan, including the free one. Nothing here is an add-on.

A retry does not send it twice

Send an idempotency_key and the repeat comes back with the id of the original.

Send it later, cancel it before it goes

Set send_at and the message leaves then. Cancel any time before it does.

Bounces stop themselves

A hard bounce or a complaint suppresses the address, and the next send to it is refused.

Webhooks you can replay

A signed POST on every event, retried with backoff, and replayable when your endpoint was down.

Up to 100 sends in one request

One round trip instead of a hundred, and an id back for each.

Four addresses that never send

Nothing reaches the internet, but you still get a real id, real events, and real webhooks.

Domain setup in one screen

Copy five DNS records, then check them from the same page. Your mail is signed as you from then on.

Your DNS, once 3 of these are the signature
CNAME <token>._domainkey.mail.acme.com <token>.dkim.amazonses.com DKIM
TXT _emailssh.mail.acme.com emailssh-verify=<token> ownership
MX mail.acme.com 10 inbound-smtp.eu-west-1.amazonaws.com inbound
TXT mail.acme.com v=spf1 include:amazonses.com ~all SPF
TXT _dmarc.mail.acme.com v=DMARC1; p=none; DMARC
Authentication-Results, on a message that arrived
dkim=pass header.i=@mail.acme.com
spf=pass smtp.mailfrom=bounce@mail.acme.com
dmarc=pass header.from=mail.acme.com

Common questions

Domains, pricing, and the mail that comes back.

What is emails.sh?

emails.sh is a transactional email API for developers. Your app sends verification links, password resets, receipts, and alerts with one POST to https://emails.sh/v1/emails, from a domain you have verified, and every message gets an id whose delivery history you can look up later.

How is emails.sh different from Resend or Postmark?

The API is the same shape, so a migration is a base URL and a key. Two things differ. Receiving is included: mail sent back to your domain is parsed, threaded, and posted to a webhook rather than discarded. And every integration path is built for a coding assistant to walk, from llms.txt to markdown docs to an installable skill.

How do I add email to my app with Cursor or Claude Code?

Paste this at the assistant: "Add emails.sh to this project. Read https://emails.sh/llms.txt" The assistant fetches the machine-readable docs, writes the call in your framework, installs the SDK, and tells you which DNS records to add.

What does a send actually look like?

A POST to https://emails.sh/v1/emails with an Authorization: Bearer esh_... header and a JSON body of from, to, subject, and html. The response is an id and a status of queued. That one endpoint is enough to run a product on for years.

Which languages have a client?

Node and TypeScript, Python, PHP, Ruby, Go, Rust, Java, C# and .NET, and Elixir, plus curl for anything else. Framework guides sit on top of those for Next.js, Nuxt, SvelteKit, Remix, Astro, Laravel, Rails, Django, FastAPI, Spring, Phoenix, Express, Hono, and Cloudflare Workers.

How long does the first send take?

A test send from onboarding@emails.sh works the moment you have a key, which is about two minutes from signing up. Sending from your own domain takes as long as your DNS provider needs to publish the records, usually minutes.

Send your first email in about two minutes.

3,000 emails a month, free. No credit card.