# Sending (/docs/code/emails/sending)

---
title: Sending
description: ctx.email is a MailerService with three methods. sendTemplate renders a registered template and sends it, send takes a raw message, and renderTemplate stops at the HTML.
kind: reference
package: questpie
---

| Method                    | Takes                            | Returns                |
| ------------------------- | -------------------------------- | ---------------------- |
| `sendTemplate(options)`   | A template key and typed `input` | `Promise<void>`        |
| `send(options)`           | A raw subject and body           | `Promise<void>`        |
| `renderTemplate(options)` | A template key and typed `input` | `Promise<EmailResult>` |

`ctx.email` is on every handler context: hooks, routes, jobs, services, and
email handlers. `app.email` is the same object outside a handler. Its type
carries no template keys, so `sendTemplate` there takes a plain `string` and an
unchecked `input`.

## sendTemplate

Renders a registered template, then hands the result to `send`. Use it for
anything you declared in `emails/`.

```ts
await ctx.email.sendTemplate({
	template: "welcome",
	input: { name: "Ada", activationUrl: "https://acme.test/activate/abc" },
	to: "ada@example.com",
});
```

| Option      | Type                      | Notes                                          |
| ----------- | ------------------------- | ---------------------------------------------- |
| `template`  | `keyof AppEmailTemplates` | Required. The camelCased filename.             |
| `input`     | inferred from the schema  | Required. Parsed by the template's Zod schema. |
| `to`        | `string \| string[]`      | Required.                                      |
| `subject`   | `string`                  | Overrides the rendered subject.                |
| `from`      | `string`                  | Falls back to `defaults.from`.                 |
| `cc`, `bcc` | `string \| string[]`      |                                                |
| `replyTo`   | `string`                  |                                                |
| `locale`    | `string`                  | Reaches the handler as `args.locale`.          |
| `ctx`       | `{ app?, db?, session? }` | Escape hatch outside a request or job scope.   |

<Callout type="info" title="No attachments here">
	`sendTemplate` has no `attachments` and no `headers`. To send a template with
	either, call `renderTemplate` first, then pass the result to `send` with your
	extras.
</Callout>

## send

Takes a message you assembled yourself. This is the only method that carries
attachments and custom headers.

```ts
await ctx.email.send({
	to: "ada@example.com",
	subject: "Your export is ready",
	html: "<p>Download it <a href='https://acme.test/x'>here</a>.</p>",
	attachments: [
		{ filename: "report.csv", content: csvBuffer, contentType: "text/csv" },
	],
});
```

| Option        | Type                                                           | Notes                                             |
| ------------- | -------------------------------------------------------------- | ------------------------------------------------- |
| `to`          | `string \| string[]`                                           | Required.                                         |
| `subject`     | `string`                                                       | Required by the type.                             |
| `html`        | `string`                                                       | One of `html` or `text` must be there.            |
| `text`        | `string`                                                       | Derived from `html` when you omit it.             |
| `from`        | `string`                                                       | Falls back to `defaults.from`, then to a default. |
| `cc`, `bcc`   | `string \| string[]`                                           |                                                   |
| `replyTo`     | `string`                                                       |                                                   |
| `attachments` | `Array<{ filename; content: Buffer \| string; contentType? }>` |                                                   |
| `headers`     | `Record<string, string>`                                       |                                                   |

Pass neither `html` nor `text` and it throws `No text or html provided`.

## renderTemplate

Runs the handler and returns `{ subject, html, text? }`. It never touches the
adapter, so nothing is delivered.

```ts
const rendered = await ctx.email.renderTemplate({
	template: "welcome",
	input: { name: "Ada", activationUrl: "https://acme.test/activate/abc" },
});

await ctx.email.send({
	...rendered,
	to: "ada@example.com",
	attachments: [{ filename: "terms.pdf", content: pdf }],
});
```

It takes `template`, `input`, `locale` and `ctx`. The rest of the send options
do not apply.

## What gets serialized

Every adapter receives a `SerializableMailOptions`, which is your message with
three fields settled.

| Field  | After serialization                                           |
| ------ | ------------------------------------------------------------- |
| `from` | Your value, else `defaults.from`, else `noreply@example.com`. |
| `text` | Your value, else converted from `html`, else `""`.            |
| `html` | Your value, else `""`.                                        |

`from` is always a real address. `text` and `html` are typed `string`, but the
one you did not supply arrives empty. A text-only message reaches the adapter
with `html: ""`.

## What each call throws

| Error                                          | Cause                                   |
| ---------------------------------------------- | --------------------------------------- |
| `Template "X" not found.`                      | No template registered under that key.  |
| A `ZodError`                                   | `input` failed the template's schema.   |
| `Email handler for "X" must return a subject`  | The handler returned a falsy `subject`. |
| `Email handler for "X" must return html`       | The handler returned falsy `html`.      |
| `email template 'X' handler needs app context` | No ambient scope, and no `ctx` passed.  |
| `No text or html provided`                     | `send` got neither body.                |

The first five come from `renderTemplate`, so they reach `sendTemplate` too.
It renders before it sends, and nothing goes out when one of them fires.

## Types

Template keys and input types are inferred from your files, so `sendTemplate`
needs no annotations. To name a template's input type elsewhere, import the
template and unwrap it.

```ts
import type { InferEmailTemplateInput } from "questpie";
import type welcome from "@/questpie/server/emails/welcome";

type WelcomeInput = InferEmailTemplateInput<typeof welcome>;
// { name: string; activationUrl: string }
```

The rest come from `questpie/mailer`, and from the root `questpie` export
as well.

| Type                      | What it is                                |
| ------------------------- | ----------------------------------------- |
| `EmailTemplateDefinition` | The `{ name, schema, handler }` shape.    |
| `EmailResult`             | `{ subject, html, text? }`.               |
| `EmailHandlerArgs`        | `AppContext & { input, locale? }`.        |
| `MailOptions`             | The argument to `send`.                   |
| `SerializableMailOptions` | What every adapter receives.              |
| `MailerConfig`            | The `email` block in your runtime config. |
| `InferEmailTemplateInput` | Pulls the input type off a template.      |
| `EmailTemplateNames`      | The keys of a templates record.           |
| `GetEmailTemplate`        | Indexes a templates record by key.        |

Codegen also emits `AppEmailTemplates`, the record of every template in the
app. With no `emails/` files it is `Record<string, never>`, which is why
`sendTemplate` has nothing to autocomplete in a fresh project.
