Send email from Cursor

Cursor has a terminal and can edit every file in your project, so adding transactional email is one prompt rather than a tutorial. The prompt below tells Cursor where the API reference lives, which environment variable to read, and what to do if you have not made a key yet. Everything it writes runs on the server, so the key never reaches the browser.

Paste into Cursor (Cmd+I)

Copy this. The page it names is served as markdown at that exact URL, so the assistant reads the real integration rather than guessing at an API shape.

Paste into Cursor (Cmd+I)
Add transactional email to this project using emails.sh.

Read https://emails.sh/docs.md first so you use the real request shape instead of guessing it.

1. Install the SDK with `npm install @emails.sh/sdk` in the project root.
2. Add EMAILSSH_API_KEY to .env.local and make sure .env.local is in .gitignore. If I have not given you a key, stop and tell me to create one at https://emails.sh/dashboard, then continue once I paste it.
3. Write the send in server-only code (a route handler, server action, or API route). Never import @emails.sh/sdk into a client component and never put the key in code that ships to the browser.
4. Send from onboarding@emails.sh until I have verified my own domain, then switch the from address to that domain.
5. Wire it into my signup handler so a new account gets a verification email, and show me the file you changed.
01

Cursor fetches the reference.

It reads https://emails.sh/docs.md, which is the same page as the HTML docs served as markdown, so it gets the exact field names for POST /v1/emails instead of a plausible-looking invention.

02

It installs the SDK and sets the key.

`npm install @emails.sh/sdk` in the project root, then EMAILSSH_API_KEY=esh_... in .env.local, with a check that .env.local is ignored by git.

03

It writes one server-side send.

A single module that constructs the client from process.env.EMAILSSH_API_KEY and exposes a function your handlers call. Nothing about the key is duplicated across files.

04

It calls that function from your signup handler.

The new account gets a verification link, the response id comes back from the API, and you can look that id up at https://emails.sh/dashboard to see whether it was delivered.

What Cursor writes

src/lib/email.ts, plus the two lines that call it from your signup handler.

What Cursor writes
import { Emailssh } from '@emails.sh/sdk';

// Constructed once at module load. This file must never be imported from a
// client component, because that would bundle the key into the browser build.
const emails = new Emailssh(process.env.EMAILSSH_API_KEY!);

export async function sendVerificationEmail(to: string, token: string) {
	const link = `https://acme.com/verify?token=${token}`;

	const { id } = await emails.send({
		// Use onboarding@emails.sh until your own domain is verified.
		from: 'Acme <hello@acme.com>',
		to: [to],
		subject: 'Confirm your email',
		html: `<p>Confirm your address to finish signing up.</p>
<p><a href="${link}">Confirm your email</a></p>`,
		text: `Confirm your address to finish signing up: ${link}`,
		// Two clicks on the signup button should not send two emails. The same
		// key within the retry window returns the original id instead.
		idempotencyKey: `verify:${token}`
	});

	return id;
}

Worth knowing

01

Cursor will inline the key if you let it

Ask for a send inside a React component and you get a fetch with the key in the request, which ships to every visitor. Say "server-side only" in the prompt, and if a key ever lands in client code, revoke it at https://emails.sh/dashboard and issue a new one. Revoking takes effect immediately.

02

It reaches for nodemailer and Gmail by default

Nodemailer with an app password works on your laptop and fails on Vercel, Netlify, and Cloudflare, because those runtimes block outbound port 587. Naming emails.sh in the prompt avoids the detour entirely.

03

Without the docs URL it invents fields

Assistants confidently write `body` or `recipients` because other providers use them. emails.sh takes `to` as an array, `html`, and `text`. The .md URL in the prompt is what stops the guess.

04

Preview deployments need the variable too

Setting EMAILSSH_API_KEY locally does not set it on your host. Add it to production and preview environments separately, or preview builds throw on the first send.

05

Your assistant can run the account, not just write the code

There is an MCP server at https://mcp.emails.sh. Connect it and the assistant gets 19 tools for the things you would otherwise alt-tab to a dashboard for: add a domain and read back the exact DNS rows, trigger a verification check, mint or revoke a scoped key, send a test message, read a delivery timeline, work out why something bounced, list and lift suppressions, and create, test, or replay a webhook. Authenticate with an esh_ key as a bearer token, or with OAuth. Revoking a key and lifting a suppression are marked destructive and need an explicit confirmation before they run.

06

It can write your lifecycle sequences as a file

An automation here is a YAML document: a trigger, an optional filter, and a list of steps that each send, wait, or branch. GET https://emails.sh/v1/automations/<id>.yaml returns it, PUT the same path replaces it, and a document you push is stored as the exact bytes you sent. So an assistant can write a trial sequence into your repository, you review the diff like any other change, and CI pushes it on merge. Errors from the parser name the wrong thing, say what to write instead, and give a line number, which is what lets an assistant correct itself. Note that this runs over the REST API rather than over MCP: there are no automation tools on the MCP server.

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 address to finish signing up: https://acme.com/verify?token=8f2c1a

Questions

Do I need a verified domain before the first send?

No. Send from onboarding@emails.sh while you are building. Verify your own domain when you want the from address to be yours, which takes two DNS records.

Where does Cursor get my API key?

It does not. You create one at https://emails.sh/dashboard and paste it into .env.local. The prompt tells Cursor to stop and ask rather than fabricate a key.

Can Cursor use the API without an SDK?

Yes. POST to https://emails.sh/v1/emails with an Authorization: Bearer esh_... header. The SDK is types and retries around that one call.

How do I check whether the email actually arrived?

The send returns { "id": "...", "status": "queued" }. GET /v1/emails/:id returns the delivery events for that id, and the same history is in the dashboard.