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.

Integration or environment variable

Both exist side by side. The rule is simple:

SituationUse
A supported service, standard usage (sending mail, verification codes)Connect the integration, call ctx.email.*
An unsupported service, or a provider-specific advanced APIPut 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 and verify your sending domain there.
  2. Open Backend → Integrations in the editor and paste the API key into the Resend card. Saving calls Resend for real to validate the key, so an invalid key fails immediately instead of at send time in production.
  3. Pick the sending address from the verified domains that were read back, for example noreply@yourdomain.com. Sender name and reply-to are optional.
  4. 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:

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.

Email verification codes

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

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 platformDefault
Random numeric code length6 digits
Expiry10 minutes (configurable up to 60)
Single use: consumed on a successful checkYes
Constant-time comparison, no prefix leakYes
Wrong-attempt cap per code, then invalidated5 attempts
Send rate per recipient per scene5 per 10 minutes
Daily verification emails per project500, 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.

What the integration lets you configure

SettingEffect
Sending addressDefault from; must come from a verified domain
Sender nameDisplay name the recipient sees
Reply-to addressDefault replyTo
Verification email subjectSupports the {{code}} placeholder
Code expiry10 minutes by default, 60 maximum
Code length6 digits by default
Daily capVerification 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.

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.

Render diagnostics