Transactional email for vibe coders

Your app works and now it has to email someone. The fastest route is to give your assistant the docs URL and the environment variable name rather than reading a docs site yourself. Every assistant page on this site carries the exact text to paste, and every page is also available as markdown at the same address with .md on the end, which is what the assistant reads.

01

Make a key.

https://emails.sh/dashboard, one click, it starts with esh_. Free tier is 3,000 emails a month and 100 a day, no card.

02

Paste the instruction for your assistant.

Cursor (/for/cursor), Claude Code (/for/claude-code), Lovable (/for/lovable), Bolt (/for/bolt), v0 (/for/v0), Replit (/for/replit), Windsurf (/for/windsurf), GitHub Copilot (/for/github-copilot). Each paste names the docs URL and the environment variable, which is what stops the assistant inventing an API.

03

Put the key where that tool keeps secrets.

.env.local in a local project, the Secrets pane in Replit, the Cloud panel secrets in Lovable, project environment variables in v0. Never in a file you commit and never in code the browser downloads.

04

Send one real email, then check the id.

The response is { "id": "...", "status": "queued" }. That id is searchable in the dashboard, so "did it arrive" has an answer that is not a guess.

The whole integration

The one call underneath every assistant page on this site.

The whole integration
import { Emailssh } from '@emails.sh/sdk';

// The key comes from the environment, never from a literal in the file. That
// single habit is what keeps it out of your git history and your browser
// bundle.
const emails = new Emailssh(process.env.EMAILSSH_API_KEY!);

const { id, status } = await emails.send({
	// onboarding@emails.sh works before you have verified a domain of your own.
	from: 'Acme <onboarding@emails.sh>',
	to: ['new.user@example.com'],
	subject: 'Confirm your email',
	html: '<p><a href="https://acme.com/verify?token=abc123">Confirm your email</a></p>',
	text: 'Confirm your email: https://acme.com/verify?token=abc123'
});

console.log(id, status); // "eml_...", "queued"

Worth knowing

01

The key belongs on the server, always

If it is in a React component, a mobile app, or anything with NEXT_PUBLIC_ or VITE_ in front of it, it is public. Move the send behind a route or function of your own and revoke the exposed key in the dashboard.

02

Gmail SMTP is a dead end for a deployed app

App passwords, a 500 message daily cap, and hosts that block port 587. It works on your laptop and stops working the day you deploy, which is the worst possible time to find out.

03

Verify your domain before you tell anyone about the app

Two DNS records. Until then mail comes from onboarding@emails.sh, which is fine for testing and wrong for a launch.

04

Point the assistant at the .md, not the HTML

https://emails.sh/with/next.js.md and every other page in .md form exist so an assistant reads the page instead of guessing at your provider. https://emails.sh/llms.txt lists them all.

What arrives

One call to POST /v1/emails, and this is the message. The delivery result for it is on GET /v1/emails/:id a second later.

Sent
To:      new.user@example.com
Subject: Confirm your email

Confirm your email: https://acme.com/verify?token=abc123

Questions

I do not know what SPF and DKIM are. Does that block me?

No. Adding a domain shows you the exact records to paste at your registrar, and the dashboard says verified or not. You can send from onboarding@emails.sh in the meantime.

My assistant wrote code that does not work.

It almost certainly invented the request body. Send it https://emails.sh/docs.md and ask it to correct the call. The real shape is { from, to: [], subject, html, text }.

How much does this cost while I am building?

Nothing. 3,000 emails a month on the free tier, no credit card. Pro is $20 a month for 50,000.

Do I need a backend?

You need somewhere server-side for the key: a route handler, a server action, an edge function. Every platform on this list has one.