Send email from Claude Code
Claude Code runs commands, reads your whole repository, and can fetch a URL, which is everything it needs to add transactional email without you opening a docs site. There is also an installable skill, so the API shape is in context before the first file is written.
Paste into Claude Code
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.
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. https://emails.sh/llms.txt lists the rest of the pages if you need a framework guide for this stack.
1. Run `npm install @emails.sh/sdk` (or `pip install emailssh` if this is a Python project).
2. Put EMAILSSH_API_KEY in the env file this project already uses, and confirm that file is gitignored. If no key exists, stop and tell me to create one at https://emails.sh/dashboard rather than inventing a placeholder that looks real.
3. Add one server-side module that reads the key from the environment and exposes a send function. Do not scatter the key across files and do not commit it.
4. Call that function from the signup handler to send a verification email, from onboarding@emails.sh until my own domain is verified.
5. Show me the diff and tell me how to test it once, without sending to real users.Claude Code reads the repo before it writes.
It works out whether this is Next.js, Express, Django, or something else, and puts the send where that framework expects server code to live.
It installs the client for your language.
`npm install @emails.sh/sdk` for node and typescript, `pip install emailssh` for python. Both wrap the same POST /v1/emails.
It adds the key to the env file you already have.
EMAILSSH_API_KEY goes next to your database URL, not into a new file, and it checks the file is ignored by git before writing a secret into it.
It wires the send and tells you how to test it.
One call in the signup path, sending to your own address first, and the returned id to look up in the dashboard.
What Claude Code writes
app/api/signup/route.ts in a Next.js project, complete and runnable.
import { Emailssh } from '@emails.sh/sdk';
import { NextResponse } from 'next/server';
const emails = new Emailssh(process.env.EMAILSSH_API_KEY!);
export async function POST(request: Request) {
const { email } = await request.json();
// Your own code: create the account and mint a single-use token.
const token = crypto.randomUUID();
const link = `https://acme.com/verify?token=${token}`;
const { id, status } = await emails.send({
from: 'Acme <hello@acme.com>',
to: [email],
subject: 'Confirm your email',
html: `<p>Welcome to Acme.</p><p><a href="${link}">Confirm your email</a></p>`,
text: `Welcome to Acme. Confirm your email: ${link}`,
// Tags show up in the dashboard, so you can filter every verification
// send when someone reports that signup mail is not arriving.
tags: { type: 'verification' },
idempotencyKey: `verify:${token}`
});
// status is "queued" here. Delivery events arrive later, and GET
// /v1/emails/${id} is what tells you whether it landed.
return NextResponse.json({ emailId: id, status });
}Worth knowing
Install the skill and it stops fetching docs
The Claude Code and Cursor skill puts the whole API in context, so the first send is written correctly with no network round trip. Without it, the .md URL in the prompt does the same job one fetch at a time.
A fabricated key looks exactly like a real one
Keys start with esh_, a pattern any model can imitate. Telling Claude Code to stop and ask means you get a question rather than a 401 forty minutes later.
It may add a retry loop you do not need
The SDK already retries on 5xx and network failures. A second retry layer on top turns one transient error into several sends unless idempotencyKey is set on every call.
Check .gitignore before the first commit
Claude Code writes the key where you tell it to. If that file is tracked, the key is in your history. Ask it to confirm the ignore rule in the same turn.
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.
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.
To: you@example.com
Subject: Confirm your email
Welcome to Acme. Confirm your email: https://acme.com/verify?token=6d0b93Questions
What is the skill and do I need it?
It is an installable Claude Code and Cursor skill that carries the emails.sh API reference. You do not need it: fetching https://emails.sh/docs.md gets the same information. The skill only saves the fetch.
Can it set up my domain too?
It can add the domain through POST /v1/domains and print the DNS records, but you have to add those records at your registrar yourself. Verification usually completes within an hour of the records propagating.
Does this work for a Python project?
Yes. `pip install emailssh` and the same call shape. The prompt above tells it to pick based on what the repository already uses.
How do I test without emailing real users?
Send to your own address, and set a tag like { "env": "dev" } so development sends are one filter away in the logs.