Documentation

Templates and OTP

Create versioned templates, preview and test them, send from a template, and use the built-in one-time-code challenge flow.

Search Docs

StackShift Mail

Templates and OTP

Live

Create versioned templates, preview and test them, send from a template, and use the built-in one-time-code challenge flow.

Goal

Use reusable email templates and OTP challenges without storing plaintext OTP codes in your application.

Current status

Live

This area is documented as current, user-reliable behavior.

Workflow

  1. 1Create a template with name, slug, subject, html, and/or text.
  2. 2Preview with data before sending so missing variables are visible.
  3. 3Send by template slug or id and optionally pin versionId.
  4. 4Use OTP send and verify for challenge-based authentication flows.
  5. 5Inspect OTP challenges by to, purpose, status, cursor, and limit.

Template lifecycle

  • Templates have active, archived, or deleted status.
  • Every create or content update creates template version data with subject, html, text, variables, and versionNumber.
  • A template can expose activeVersion and versions in detail responses.
  • Preview renders subject, html, and text and returns missingVariables.
  • Test send returns messageId and status.

Create, preview, and send a template

Example

ts
await stackshift.mail.templates.create({
  name: 'Welcome email',
  slug: 'welcome-email',
  subject: 'Welcome, {{firstName}}',
  html: '<h1>Welcome, {{firstName}}</h1>',
  text: 'Welcome, {{firstName}}',
})

const preview = await stackshift.mail.templates.preview('welcome-email', {
  data: { firstName: 'Ada' },
})

const sent = await stackshift.mail.sendTemplate({
  template: 'welcome-email',
  from: 'noreply@example.com',
  to: 'ada@example.net',
  data: { firstName: 'Ada' },
  idempotencyKey: 'welcome_user_123_v1',
})

console.log(preview.missingVariables, sent.id)

OTP challenge flow

Example

ts
const challenge = await stackshift.mail.otp.send({
  to: 'ada@example.net',
  from: 'security@example.com',
  purpose: 'login',
  expiresIn: '10m',
  codeLength: 6,
  brandName: 'Example App',
  idempotencyKey: 'login_ada_2026_05_10',
})

const result = await stackshift.mail.otp.verify({
  challengeId: challenge.id,
  code: '123456',
})

console.log(result.verified, result.status)

OTP challenge states

  • Challenge status is pending, verified, expired, failed, or canceled.
  • Responses include attempts, maxAttempts, attemptsRemaining, expiresAt, resendAvailableAt, and optional messageId.
  • Admin-style challenge APIs can list, fetch, and cancel challenges.

Expected result

Template sends are rendered server-side, missing variables fail before queueing, and OTP challenges record status and attempt counts.

Common failures

  • Missing template variables in data.
  • Sending with a deleted or archived template.
  • Verifying an OTP after expiration, cancellation, too many attempts, or wrong challenge context.