> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sequenzy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Transactional Emails

> Send programmatic, per-user emails via API

# Transactional Emails

Transactional emails are triggered programmatically for individual users—order confirmations, password resets, account notifications, and other time-sensitive communications.

## Transactional vs Marketing

| Transactional            | Marketing           |
| ------------------------ | ------------------- |
| Triggered by user action | Sent to segments    |
| One recipient at a time  | Multiple recipients |
| Time-sensitive           | Can be scheduled    |
| Expected by user         | Promotional         |
| Higher deliverability    | May be filtered     |

**Examples of transactional emails:**

* Order confirmation
* Shipping notification
* Password reset
* Account verification
* Payment receipt
* Security alert

## Sending Modes

### 1. Template Mode (Recommended)

Create reusable templates in the dashboard, send via API using the template slug.

```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "customer@example.com",
    "slug": "order-confirmation",
    "variables": {
      "orderNumber": "ORD-12345",
      "total": "$99.99",
      "deliveryDate": "January 20, 2024"
    }
  }'
```

**Benefits:**

* Non-developers can edit templates
* Consistent branding
* Version control in dashboard
* Analytics per template
* Template blocks can include conditional display rules based on recipient variables
* Repeat blocks can render one child block set per item in an array variable

### Dynamic Array Data

Transactional templates can repeat blocks over arrays passed in `variables`.
For example, a repeat block with source `items` and item alias `item`
renders its child blocks once per item. Inside those children, use scoped merge
tags like `{{item.title}}`, `{{item.description}}`, and `{{item.number}}`.

```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "customer@example.com",
    "slug": "order-confirmation",
    "variables": {
      "items": [
        { "title": "Pro plan", "description": "Monthly subscription" },
        { "title": "Priority support", "description": "Account add-on" }
      ]
    }
  }'
```

#### Nested Repeats

Repeat blocks can be nested. Point the inner repeat block's source at an array
inside each parent item using the parent's item alias - it does not need a
separate top-level array. For example, with an outer repeat over `orders` (alias
`order`), an inner repeat with source `order.lineItems` (alias `line`) renders
once per line item of the current order, and its children can use merge tags
like `{{line.title}}`.

```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "customer@example.com",
    "slug": "order-confirmation",
    "variables": {
      "orders": [
        {
          "number": "ORD-12345",
          "lineItems": [
            { "title": "Pro plan" },
            { "title": "Priority support" }
          ]
        }
      ]
    }
  }'
```

### Conditional Blocks

Conditional (if/else) blocks show one set of blocks when a rule matches and an
optional fallback otherwise. In the editor, add a condition and pick the
**Custom variable** field to branch on a value passed in your request `variables`
or an automation `event` payload - not just stored subscriber data.

The variable name is the same reference you would use in a `{{merge tag}}`, so
nested paths work too (for example `order.total` or `event.plan`). You can match
with equals, contains, numeric comparisons (`>=`, `<=`, `>`, `<`), or check
whether the variable is present with "is empty" / "is not empty".

For example, sending with `"variables": { "plan": "pro" }` will render the IF
branch of a condition like `plan equals pro`, and the OTHERWISE branch for any
other value.

Conditions can also target stored subscriber data - segment membership, custom
events, tags, lists, status, engagement, and purchases. These rules are checked
against the recipient's subscriber profile at send time; if the recipient is
not a stored subscriber, they see the OTHERWISE branch.

### 2. Direct Mode

Send email content directly without a pre-created template.

```bash theme={null}
curl -X POST "https://api.sequenzy.com/api/v1/transactional/send" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "customer@example.com",
    "subject": "Your Order #{{orderNumber}}",
    "body": "<h1>Thank you for your order!</h1><p>Order: {{orderNumber}}</p>",
    "variables": {
      "orderNumber": "ORD-12345"
    }
  }'
```

**Use cases:**

* Dynamic content generated by your app
* One-off emails that don't need templates
* Testing and development
* React Email templates (see below)

### 3. React Email Mode

Build type-safe, responsive email templates using [React Email](https://react.email) components and render them to HTML.

```typescript theme={null}
import { render } from "@react-email/render";
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Html,
  Preview,
  Text,
} from "@react-email/components";

// Create your email component
function OrderConfirmation({ orderNumber, total }: { orderNumber: string; total: string }) {
  return (
    <Html>
      <Head />
      <Preview>Your order #{orderNumber} is confirmed</Preview>
      <Body style={{ fontFamily: "sans-serif" }}>
        <Container>
          <Heading>Thank you for your order!</Heading>
          <Text>Order: #{orderNumber}</Text>
          <Text>Total: {total}</Text>
          <Button href="https://example.com/orders">View Order</Button>
        </Container>
      </Body>
    </Html>
  );
}

// Render and send
const html = await render(OrderConfirmation({ orderNumber: "ORD-12345", total: "$99.99" }));

await fetch("https://api.sequenzy.com/api/v1/transactional/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    to: "customer@example.com",
    subject: "Your Order #ORD-12345",
    body: html,
  }),
});
```

**Benefits:**

* Type-safe templates with TypeScript
* Reusable components
* Responsive design built-in
* Preview emails during development

### 4. SMTP Template Payloads

If another product can send through SMTP but lets you control the message body, you can point it at Sequenzy SMTP and send a JSON payload that references one of your existing transactional emails.

This is useful for products such as Supabase Auth, where Supabase still triggers the email but Sequenzy renders and sends the final template.

**Requirements:**

* A Sequenzy API key for SMTP authentication
* At least one sender profile on a verified sending domain
* A transactional email with a sender profile assigned

```json theme={null}
{
  "slug": "supabase-magic-link",
  "dataVariables": {
    "confirmation_url": "https://app.example.com/auth/verify?token=123",
    "email": "user@example.com"
  }
}
```

You can also use `transactionalId` instead of `slug`. If Sequenzy receives a JSON body in this format over SMTP, it ignores the raw email subject/body and sends the saved transactional template instead.

For the full Supabase setup, including variable mapping, see the [Supabase integration guide](/integrations/supabase).

<Card title="Framework Quickstarts" icon="rocket" href="/send-email/nextjs">
  See our quickstart guides for [Bun](/send-email/bun),
  [Next.js](/send-email/nextjs), and [Express](/send-email/express).
</Card>

## Creating Templates

### In the Dashboard

1. Go to **Transactional** in your dashboard
2. Click **Create Template**
3. Enter a **slug** (URL-friendly identifier)
4. Design your email with the visual editor
5. Add variable placeholders where needed
6. Save and activate

### Template Slugs

Slugs are unique identifiers for your templates:

```
order-confirmation
password-reset
welcome-email
invoice-reminder
shipping-update
```

<Note>
  Slugs are auto-generated from the template name but can be customized. They
  cannot be changed after creation.
</Note>

## Variables

Variables let you personalize transactional emails.

### Syntax

```html theme={null}
<p>Hello {{FIRST_NAME}},</p>
<p>Your order {{orderNumber}} has shipped!</p>
```

### With Defaults

Provide fallback values for missing variables:

```html theme={null}
<p>Hello {{FIRST_NAME|Customer}},</p>
<p>Hello {{ firstName | default: "Customer" }},</p>
```

If `FIRST_NAME` or `firstName` is empty, "Customer" is used.

If a variable has no default and no value is provided, it renders as an empty
string. The transactional email is still sent.

### Conditional HTML Sections

Use `{{#if variable}}`, `{{else}}`, and `{{/if}}` to render simple conditional
sections in raw HTML. This is most useful with subscriber custom attributes:

```html theme={null}
{{#if subscriber.plan}}
<p>Your {{subscriber.plan}} workspace is ready.</p>
{{else}}
<p>Your workspace is ready.</p>
{{/if}}
```

`{{#unless variable}}...{{/unless}}` is also supported. Conditions check
whether the resolved variable is present and truthy. Nested `if`/`unless`
sections are supported, but helpers, comparisons, loops, and arbitrary
Handlebars expressions are not.

### System Variables

These variables are resolved at send time:

| Variable         | Source                                                                                 |
| ---------------- | -------------------------------------------------------------------------------------- |
| `{{EMAIL}}`      | Recipient email address                                                                |
| `{{FIRST_NAME}}` | Request `firstName` aliases, then the matched subscriber's stored first name           |
| `{{LAST_NAME}}`  | Request `lastName` aliases, then the matched subscriber's stored last name             |
| `{{NAME}}`       | Request `name`/`fullName`, or a full name built from the resolved first and last names |

<Note>
  Single-recipient REST API sends match a subscriber by `subscriberExternalId`
  when provided, then by recipient email. Saved first and last names fill
  missing name variables automatically. Explicit request variables remain
  authoritative. Multi-recipient sends do not use one subscriber's name for the
  whole message.
</Note>

### Custom Attributes

Any subscriber custom attributes are available:

```html theme={null}
<p>Your plan: {{plan}}</p>
<p>Account ID: {{userId}}</p>
```

### Passed Variables

Variables passed in the API request:

```json theme={null}
{
  "variables": {
    "orderNumber": "ORD-123",
    "total": "$99.99"
  }
}
```

```html theme={null}
<p>Order: {{orderNumber}}</p>
<p>Total: {{total}}</p>
```

## API Reference

### Send Email

```bash theme={null}
POST /api/v1/transactional/send
```

**Template mode:**

```json theme={null}
{
  "to": "user@example.com",
  "slug": "template-slug",
  "variables": {
    "key": "value"
  }
}
```

**Direct mode:**

```json theme={null}
{
  "to": "user@example.com",
  "subject": "Email Subject",
  "body": "<html>Email content</html>",
  "variables": {
    "key": "value"
  }
}
```

The REST API keeps `slug` and `body` as the canonical fields. For compatibility
with MCP-style callers, `templateId` is accepted as an alias for the
transactional API slug and `html` is accepted as an alias for `body`. If both a
canonical field and its alias are provided, their values must match.

### Marketing delivery policy

The endpoint defaults to `"emailType": "transactional"`. For a consented
lifecycle or promotional message, set `"emailType": "marketing"`. Marketing
mode supports one recipient with no CC/BCC, creates or links a minimal
subscriber, honors unsubscribe suppression, adds the standard marketing
footer, and emits RFC 8058 one-click-unsubscribe headers automatically:

```json theme={null}
{
  "to": "user@example.com",
  "slug": "trial-reminder",
  "emailType": "marketing",
  "variables": {
    "firstName": "Ana"
  }
}
```

The caller is responsible for consent or another lawful basis. Do not pass an
unsubscribe URL; Sequenzy generates recipient-specific signed URLs.

The response echoes the accepted `emailType`. If the email includes
`{{viewInBrowserUrl}}` (or `{{VIEW_IN_BROWSER_URL}}`), Sequenzy replaces it
with a hosted copy URL. Sends without an explicit `replyTo` inherit the company
default reply profile, and the resolved value is retained with the send record.

**Response:**

```json theme={null}
{
  "success": true,
  "emailSendId": "send_abc123",
  "emailType": "marketing",
  "jobId": "job_abc123",
  "to": "user@example.com"
}
```

Use the durable `emailSendId` with
`GET /api/v1/email-sends/{emailSendId}` to read one message's delivery status,
open/click timestamps, and event timeline. For aggregate Send API rates, call
`GET /api/v1/metrics?period=30d&emailType=transactional` or run
`sequenzy stats --email-type transactional --period 30d`.

To discover recent deliveries by subject/title, recipient, or status, call
`GET /api/v1/email-sends?search=trial-reminder&status=opened` or run
`sequenzy email-sends list --search trial-reminder --status opened`. For one
saved template's aggregate rates, call
`GET /api/v1/metrics/transactional/{idOrSlug}` or run
`sequenzy stats --transactional <id-or-slug>`. The template-level result also
includes top clicked links, complaints, replies, latest bounce classifications,
and separate human/machine engagement counts.

### List Templates

```bash theme={null}
GET /api/v1/transactional?search=order&status=active&sort=open-rate&order=desc
```

**Response:**

```json theme={null}
{
  "success": true,
  "transactional": [
    {
      "id": "trans_abc123",
      "slug": "order-confirmation",
      "name": "Order Confirmation",
      "emailId": "email_abc123",
      "subject": "Your order is confirmed",
      "enabled": true,
      "stats": {
        "sends": 120,
        "deliveries": 118,
        "opens": 92,
        "clicks": 40,
        "bounces": 2,
        "openRate": 77.97,
        "clickRate": 33.9
      }
    }
  ]
}
```

### Get Template Details

```bash theme={null}
GET /api/v1/transactional/:idOrSlug
```

**Response:**

```json theme={null}
{
  "success": true,
  "transactional": {
    "id": "trans_abc123",
    "slug": "order-confirmation",
    "name": "Order Confirmation",
    "emailId": "email_abc123",
    "subject": "Your Order #{{orderNumber}}",
    "blocks": [],
    "enabled": true,
    "variables": ["orderNumber", "total", "items"]
  }
}
```

## Auto-Creation

When you send to an email that doesn't exist:

1. A new subscriber is created automatically
2. Status is set to `active`
3. Custom attributes from `variables` are saved (if applicable)

This makes integration seamless—no need to create subscribers first.

## Error Handling

### Common Errors

| Error                     | Cause                     | Solution                   |
| ------------------------- | ------------------------- | -------------------------- |
| `Template not found`      | Invalid slug              | Check slug spelling        |
| `Template disabled`       | Template is deactivated   | Enable in dashboard        |
| `Missing required fields` | No `to`, `slug`/`subject` | Include required fields    |
| `No sender configured`    | No sender profile         | Set up sender in dashboard |
| `Sending paused`          | Account sending is paused | Check sending status       |

### Response Codes

| Code  | Description               |
| ----- | ------------------------- |
| `200` | Email queued successfully |
| `400` | Validation error          |
| `401` | Invalid API key           |
| `404` | Template not found        |
| `500` | Server error              |

## Best Practices

### 1. Use Templates

Templates are easier to maintain and update:

```
✓ Template: Update once, affects all future sends
✗ Direct: Must update every code path that sends email
```

### 2. Handle Variables Gracefully

Always provide defaults for optional variables, or guard larger optional
sections with conditionals:

```html theme={null}
<!-- Compact default syntax -->
<p>Hi {{FIRST_NAME|there}},</p>

<!-- Liquid-style default filter syntax -->
<p>Hi {{ firstName | default: "there" }},</p>

<!-- Conditional section syntax -->
{{#if subscriber.plan}}
<p>Your {{subscriber.plan}} workspace is ready.</p>
{{else}}
<p>Your workspace is ready.</p>
{{/if}}
```

Conditional sections support `if`, `unless`, and `else`. They do not support
comparisons or custom helpers.

### 3. Log Job IDs

Save the returned job ID for debugging:

```javascript theme={null}
const response = await sendTransactional({
  to: user.email,
  slug: "order-confirmation",
  variables: { orderNumber: order.id },
});

await saveToDatabase({
  orderId: order.id,
  emailJobId: response.data.jobId,
});
```

### 4. Use Meaningful Slugs

```
✓ "order-confirmation"
✓ "password-reset"
✓ "trial-expiring-reminder"

✗ "email1"
✗ "template_2024_01"
```

### 5. Test in Development

Use test emails before production:

```javascript theme={null}
const to =
  process.env.NODE_ENV === "development" ? "test@yourdomain.com" : user.email;
```

## Integration Examples

### Order Confirmation

```javascript theme={null}
async function sendOrderConfirmation(order) {
  await fetch("https://api.sequenzy.com/api/v1/transactional/send", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      to: order.customerEmail,
      slug: "order-confirmation",
      variables: {
        orderNumber: order.id,
        total: formatCurrency(order.total),
        items: order.items.map((i) => i.name).join(", "),
        shippingAddress: formatAddress(order.shippingAddress),
        estimatedDelivery: formatDate(order.estimatedDelivery),
      },
    }),
  });
}
```

### Password Reset

```javascript theme={null}
async function sendPasswordReset(email, resetToken) {
  const resetUrl = `${APP_URL}/reset-password?token=${resetToken}`;

  await fetch("https://api.sequenzy.com/api/v1/transactional/send", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      to: email,
      slug: "password-reset",
      variables: {
        resetUrl,
        expiresIn: "24 hours",
      },
    }),
  });
}
```

### Welcome Email

```javascript theme={null}
async function sendWelcome(user) {
  await fetch("https://api.sequenzy.com/api/v1/transactional/send", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SEQUENZY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      to: user.email,
      slug: "welcome",
      variables: {
        firstName: user.firstName,
        dashboardUrl: `${APP_URL}/dashboard`,
        docsUrl: `${APP_URL}/docs`,
      },
    }),
  });
}
```

## Related

<CardGroup cols={2}>
  <Card title="API Reference" icon="code" href="/api-reference/transactional/send">
    Full API documentation
  </Card>

  <Card title="Subscribers" icon="users" href="/concepts/subscribers">
    Auto-creation and attributes
  </Card>

  <Card title="Campaigns" icon="paper-plane" href="/concepts/campaigns">
    Broadcast marketing emails
  </Card>

  <Card title="Sequences" icon="diagram-project" href="/concepts/sequences">
    Automated email workflows
  </Card>
</CardGroup>
