# Send Email and Verification Codes with Integrations | Talizen | Talizen

> Connect Resend in the Talizen editor, then use ctx.email.send / sendCode / verifyCode in Func to send mail and verify email codes without putting credentials in your code.

[Talizen API](/)

[EN](/api/func-email-integration.md)/ [中文](/api/func-email-integration.md?lang=zh)

### Overview

- [Talizen API for AI](/api.md)

### Discoverability

- [How to optimize llms.txt](/api/optimize-llms-txt.md)

### Site configuration

- [Configure talizen.config.ts](/api/talizen-config.md)
- [Implement domain-based locale routing](/api/domain-locale-routing.md)

### Backend

- [Calling external APIs on the server and managing the cache](/api/ssr-external-api-cache.md)
- [Build site backend workflows with Func](/api/func-backend.md)
- [JSON tables: definition, reads, and queries](/api/func-json-tables.md)
- [Uploads: signed direct upload and Func-generated files](/api/func-assets-upload.md)
- [Timeouts and streaming responses](/api/func-timeout-streaming.md)
- [Integrate Alipay PC Web Payment with Func](/api/func-alipay-payment.md)

### Auth

- [Require a Verified Email to Sign Up](/api/auth-verified-registration.md)
- [Reset and Change Passwords](/api/auth-password-reset.md)
- [Sign In From a Func](/api/auth-func-login.md)
- [Query users from a Func](/api/func-user-directory.md)

### Integrations

- [Send Email and Verification Codes with Integrations](/api/func-email-integration.md)
- [Take Alipay payments with an integration](/api/func-alipay-integration.md)

### Help center

[Talizen Help](/docs/start.md)

Product guides for building sites in the editor.

IntegrationsSend Email and Verification Codes with Integrations

# Send Email and Verification Codes with Integrations

Connect the Resend integration and use ctx.email in Func for transactional email and email verification codes: the credential stays server-side, and code length, expiry, single use, constant-time comparison, attempt caps, and rate limiting are handled by the platform.

Integrations let you connect a third-party service once in the editor, then call it from Func as a platform capability. The credential stays on the server and never appears in Func code, logs, or return values. Resend is supported today, for transactional email and email verification codes.

**Scope**

This page covers the email capability: `ctx.email.send`, `ctx.email.sendCode`, and `ctx.email.verifyCode`. Alipay has its own managed integration behind `ctx.payment.alipay` — see [Take Alipay payments with an integration](/api/func-alipay-integration.md). To implement a payment flow entirely yourself (or reach a provider the platform has not adapted), see [Integrate Alipay PC Web Payment with Func](/api/func-alipay-payment.md). Note that `payment` is only a namespace: there is no unified cross-provider payment interface.

## Integration or environment variable

Both exist side by side. The rule is simple:

| Situation | Use |
| --- | --- |
| A supported service, standard usage (sending mail, verification codes) | Connect the integration, call `ctx.email.*` |
| An unsupported service, or a provider-specific advanced API | Put the key in an environment variable and call it with `fetch` |

An integration is not there to make the API call for you. It removes the "am I actually connected?" uncertainty: the key is validated for real when you save it, the sending address is picked from the domains your provider account has already verified, and you can send one real test email on the spot.

## Connect Resend

1. Create an API key in the [Resend dashboard](https://resend.com/api-keys) and verify your sending domain there.
2. Open **Backend → Integrations** in the editor, pick Resend under **Add an app**, and paste the API key. Saving calls Resend for real to validate the key, so an invalid key fails immediately instead of at send time in production.
3. Give it a **name** (used to tell several configurations of one app apart) and its **channel tags** (how Func selects this configuration; `default` if you leave it), see the next section.
4. Pick the sending address from the **verified domains** that were read back, for example `noreply@yourdomain.com`. Sender name and reply-to are optional.
5. Click **Send test email** to send a real message and confirm the whole path works.

> The sending address must belong to a domain that is **verified** in Resend. Resend rejects mail from unverified domains, which is why this is a pick-from-list rather than a free-text field.

## Send email

Once connected, call it directly in Func. No credential appears in your code:

```typescript
import type { TalizenFuncContext } from 'talizen/func-runtime'

export function notifyOwner(input, ctx: TalizenFuncContext) {
  const { id } = ctx.email.send({
    to: 'owner@example.com',
    subject: 'New booking: ' + input.name,
    html: '<p>' + input.name + ' booked ' + input.date + '</p>',
  })
  return { id }
}
```

`to` accepts one address or an array, up to 50 recipients per send. `subject` is required and at least one of `html` / `text` must be present. Returns `{ id, provider }`. Omitting `from` / `replyTo` falls back to what the integration is configured with.

## Multiple configurations and channel tags

One app can be connected **more than once** — two Resend accounts sharing the send volume, or
one for domestic and one for international traffic. Each configuration has its own name and a set of
**channel tags**, and Func picks one by tag with `via()`:

```typescript
ctx.email.send({ to, subject, html })                  // default channel, same as via("default")
ctx.email.via("otp").sendCode({ to, scene: "login" })  // send through the config tagged otp
ctx.email.via("notify").send({ to, subject, html })
```

`via()` returns **the same three methods**, just bound to a tag, so no method needs an
extra parameter.

> **Tag by purpose, not by vendor.** Use `otp` / `notify` rather than
> `resend` / `aliyun`: when that channel switches to another provider you change configuration
> only, and no Func code has to be touched.

| Situation | Behaviour |
| --- | --- |
| No `via` | Uses the configuration tagged `default` |
| Same tag on several configurations | One is chosen at random, which spreads the load |
| A configuration's key is rejected (401/403) | Another configuration with the same tag is tried; timeouts are never retried, to avoid sending twice |
| No configuration carries the requested tag | **It errors** rather than falling back to the default channel |

That last rule is deliberate. Silently falling back on a mistyped tag gives you "it sent, but through the wrong
provider" — a problem with no symptoms and no easy way to find it. Failing loudly is better.

A tag is 1–32 characters of lowercase letters, digits, or dash (the same shape as `scene`); uppercase
and spaces are rejected so you never get a tag that looks right but matches nothing.

**Codes are channel-independent**

Verification codes are stored
independently of the provider, so a code sent by `via("otp").sendCode()` verifies fine with a plain
`verifyCode()`. Switching channels, or even providers, never invalidates codes already in flight.

## Email verification codes

Do not hand-roll verification codes. `sendCode` and `verifyCode` already handle the parts that are easy to get wrong:

```typescript
import type { TalizenFuncContext } from 'talizen/func-runtime'

export function sendLoginCode(input, ctx: TalizenFuncContext) {
  ctx.email.sendCode({ to: input.email, scene: 'login' })
  return { ok: true }
}

export function verifyLoginCode(input, ctx: TalizenFuncContext) {
  const ok = ctx.email.verifyCode({
    to: input.email,
    scene: 'login',
    code: input.code,
  })
  if (!ok) throw new Error('invalid or expired code')
  return { ok: true }
}
```

`scene` namespaces codes by purpose, so a login code and a password-reset code for the same address never collide. It defaults to `default` and must be 1–32 characters of letters, digits, underscore, or dash.

| Guaranteed by the platform | Default |
| --- | --- |
| Random numeric code length | 6 digits |
| Expiry | 10 minutes (configurable up to 60) |
| Single use: consumed on a successful check | Yes |
| Constant-time comparison, no prefix leak | Yes |
| Wrong-attempt cap per code, then invalidated | 5 attempts |
| Send rate per recipient per scene | 5 per 10 minutes |
| Daily verification emails per project | 500, configurable per project (max 10000) |

> **Do not reimplement any of it:** no `Math.random()` codes, no storing codes in `ctx.db` or `ctx.cache` with your own expiry, no hand-rolled attempt counters. A wrong guess does not extend the code's lifetime — an easy detail to miss when writing this yourself.

`verifyCode` returns `false` for a wrong code, an expired code, and no code at all rather than throwing; it only errors on invalid arguments or a service failure. So `if (!ok)` is all your branch needs.

> A verified code proves control of the mailbox. It is **not** a session. Establish login state through project auth, see the [Func backend guide](/api/func-backend.md).

**These two methods are your own primitive, not proof of identity**

The `true` that `verifyCode` returns is just a value — **the platform records nothing**
**about who verified what**. Gating registration with it does not work: the registration endpoint never asks, and
skipping these two calls takes no skill at all. To make "verified" actually gate registration (or, later, password reset
and re-binding), use `startVerification` / `ctx.verify` from
[Require a Verified Email to Sign Up](/api/auth-verified-registration.md) — that outcome is recorded by the
platform and consumed by registration.

Keep these two for flows whose **outcome only means something to your own site**: order confirmation,
unsubscribe confirmation, and the like.

## What the integration lets you configure

| Setting | Effect |
| --- | --- |
| Name | Unique per project; tells several configurations of one app apart |
| Channel tags | What `via()` selects on; `default` when omitted |
| Sending address | Default `from`; must come from a verified domain |
| Sender name | Display name the recipient sees |
| Reply-to address | Default `replyTo` |
| Verification email subject | Supports the `{{code}}` placeholder |
| Code expiry | 10 minutes by default, 60 maximum |
| Code length | 6 digits by default |
| Daily cap | Verification emails per day for this project; 500 by default, 10000 maximum |

## When you need something the platform has not wrapped

By default an integration does **not** hand the credential to the Func sandbox. To use other provider APIs (Resend audiences, batch sending, attachments), or to switch providers and send it yourself, turn on **expose\_to\_func** in the integration: the key is additionally projected into `process.env` ( `RESEND_API_KEY` for Resend) and you can call anything with `fetch`.

> The toggle is off by default. Once on, Func code can read the credential — turn it on only when you actually need it.
>
> When one app is connected several times, each configuration needs a **different env var name** (for example `RESEND_API_KEY_BACKUP` for the second one), otherwise they overwrite each other in `process.env`. This is validated when you save.

## Errors and boundaries

- `ctx.email.*` errors when nothing is connected, validation has not passed, or the integration is disabled. That is a configuration action in the editor — do not work around it by calling the provider API directly.
- It errors when the integration has no sending address; set one under **Backend → Integrations**.
- An empty `subject`, empty `html` and `text`, or more than 50 recipients all return 400.
- Hitting the send rate limit returns 429 with a try-again-later message.
- All three methods are synchronous, like `ctx.db` and `ctx.cache`; writing `await` is harmless.
- Capabilities live on `ctx` only. Func has no module loader, so a value import such as `import { email } from 'talizen/func-runtime'` always fails. Use `import type` for types.

**Func fundamentals**

Files, invocation, JSON tables, auth, asset uploads, timeouts, and SSE are covered in the [Func backend guide](/api/func-backend.md).

[Previous\
\
Integrate Alipay PC Web Payment with Func](/api/func-alipay-payment.md) [Next\
\
Take Alipay payments with an integration](/api/func-alipay-integration.md)

On this page

- [Integration or environment variable](#when)
- [Connect Resend](#connect)
- [Send email](#send)
- [Multiple configurations and channel tags](#channels)
- [Email verification codes](#code)
- [What the integration lets you configure](#config)
- [When you need something the platform has not wrapped](#escape)
- [Errors and boundaries](#errors)

Looking for the help center?

[Talizen Help](/docs/start.md)

> 全站页面清单：[/llms.txt](/llms.txt)
