# Send email from a v0 app

Ask v0 for transactional email, put EMAILSSH_API_KEY in your environment variables, and send from a server action so the key stays off the client.

v0 generates Next.js, which means the correct place for a send is a server action or a route handler, and the correct place for the key is an environment variable on the project rather than a line in a component. Ask for both explicitly and the generated code is deployable as it stands.

## Paste into v0

```text
Add transactional email to this app using emails.sh, so submitting the signup form sends a verification email.

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

Requirements:
- Install @emails.sh/sdk and do the send in a server action or route handler with "use server". Never in a client component, and never with NEXT_PUBLIC_ on the key, because that prefix exposes the value to the browser.
- Read the key from process.env.EMAILSSH_API_KEY. I will add it as an environment variable on the project. If it is missing, throw an error naming the variable.
- Send from onboarding@emails.sh until my own domain is verified.
- Tell me which environment variable name to add and remind me to set it for preview as well as production.
```

## What happens next

1. **v0 adds the SDK and the server action.** @emails.sh/sdk goes into package.json, and the send lives in a file marked "use server", which is what keeps the key out of the client bundle.
1. **You add the environment variable.** EMAILSSH_API_KEY with the esh_ key from https://emails.sh/dashboard, in the project environment variables, for production and preview both.
1. **The form calls the action directly.** No fetch, no API route to maintain. The form action is the server function, and the key is read inside it.
1. **You deploy and check the id.** The action returns the id from POST /v1/emails. Looking it up in the dashboard tells you delivered or bounced without asking the recipient.

## What v0 writes

app/actions/send-verification.ts, called straight from the form.

```ts
'use server';

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

// "use server" guarantees this module never reaches the browser, which is the
// only reason it is safe to read the key here.
const emails = new Emailssh(process.env.EMAILSSH_API_KEY!);

export async function sendVerification(formData: FormData) {
	const email = String(formData.get('email'));
	const token = crypto.randomUUID();
	const link = `https://acme.com/verify?token=${token}`;

	const { id } = await emails.send({
		from: 'Acme <onboarding@emails.sh>',
		to: [email],
		subject: 'Confirm your email',
		html: `<p>Confirm your address to activate your account.</p>
<p><a href="${link}">Confirm your email</a></p>`,
		text: `Confirm your address to activate your account: ${link}`,
		tags: { type: 'verification' }
	});

	return { id };
}
```

## Worth knowing

### NEXT_PUBLIC_ on the key is a leak

That prefix is exactly what tells Next.js to inline a value into the browser bundle. EMAILSSH_API_KEY, no prefix, read only from server code.

### Preview deployments are a separate environment

Setting the variable for production leaves every preview build without it, and the first send there throws. Set it for both when you add it.

### A client component cannot import the SDK

If v0 puts the send in a component with "use client", the build either fails or ships your key. Move the call into a server action and pass the form data to it.

### Generated code often guesses the request body

Without the docs fetch you get fields from another provider. The real shape is { from, to: [], subject, html, text } and the response is { "id": "...", "status": "queued" }.

### 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.


## Questions

### Does this work in the v0 preview?

Only if the environment variable is set for that environment. The preview runs the same server code but with its own environment.

### Server action or route handler?

Either. A server action is less code when a form is the trigger. A route handler is better when something outside your app has to call it.

### Can I use my own domain?

Yes, after verification. Add the domain in the dashboard, add the two DNS records it shows, then change the from address.

### What does the free tier cover?

3,000 emails a month and 100 a day, no credit card. Enough for a project in development and a small launch.


## One send

```
To: user@example.com
Subject: Confirm your email

Confirm your address to activate your account: https://acme.com/verify?token=1b9d55
```

Docs: https://emails.sh/docs.md