# Transactional email

> Send password resets, login codes and receipts through the Broadcast API, with templates, safe retries and the right unsubscribe rules.

Source: https://sendbroadcast.net/university/developers/transactional-email

Transactional email is the mail your app sends to one person because of something they did: a password reset, a login code, a receipt. In this lesson you send one through the API, keep the content in a template, make retries safe, and learn how it differs from marketing email.

## How it differs from marketing

Broadcasts and sequences respect unsubscribe status. Transactional email does not. A password reset has to arrive whether or not the person wants your newsletter. Broadcast delivers transactional email even to someone who unsubscribed.

That power comes with a duty. Send only real service messages this way. Using the transactional route for promotions is how sending reputations get damaged.

A few addresses are still blocked. Broadcast does not send to a redacted subscriber or to an address on the global suppression list. By default, a hard bounce on the channel's own suppression list blocks marketing only. To block transactional mail too, turn on **Hard bounces also block transactional email** in the channel settings.

A transactional email carries no unsubscribe link and no `List-Unsubscribe` header, unless you send it with `include_unsubscribe_link: true`.

### Broadcasts and sequences

- Respect unsubscribe status
- Carry an unsubscribe link
- Sent to a list

### Transactional email

- Reaches people who unsubscribed
- No unsubscribe link by default
- Sent to one person, through the API

Transactional email has more reach, so keep it for real service messages.

## Step 1: Get a token

Every channel starts with an access token that has transactional permissions. Find it under **Access Tokens**. Or create one with only Transactional Emails read and write, so a leak cannot touch your campaigns.

Before you write code, open **Transactional** in the sidebar. The **Try it now** card sends a test to your own address and shows a ready-to-copy cURL command.

![The Transactional page with a Try it now card and an API Reference card holding a cURL command](https://sendbroadcast.net/assets/docs/transactional-emails/transactional-emails-empty-state-adebc3b5.png)

The Try it now card, above a cURL command you can copy.

## Step 2: Send one

```bash
curl -X POST https://broadcast.example.com/api/v1/transactionals.json \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "ada@example.com",
    "subject": "Your sign-in code",
    "body": "Your code is 123456. It expires in 10 minutes.",
    "reply_to": "support@example.com"
  }'
```

`to` is required. `subject` and `body` are required unless you use a template. The body can be plain text or HTML, and Broadcast wraps it in the HTML document for you.

The response includes an `id` and a `status_url`. `GET /api/v1/transactionals/:id.json` returns the current state. If the recipient's address matches a subscriber, the email is linked to that subscriber's record.

## Step 3: Move the content into a template

Email copy in application code is hard to change. Put it in a Broadcast template instead, and send it by id:

```json
{ "to": "ada@example.com", "template_id": 123 }
```

The template's subject, body and preheader are the defaults, and any `subject` or `body` you send overrides them. The template must belong to the token's channel. Now a marketer can fix a typo in the receipt without a deploy.

To send from a different From address, pass `sender_identity_id`. The email marketing course explains sender identities.

## Step 4: Make retries safe

Networks fail. If your client times out and retries, the person may get two receipts. Prevent that with an `Idempotency-Key` header, unique per logical send:

```
Idempotency-Key: order-12345-receipt
```

The rules:

1. The first request with a key is processed and its response is stored.
2. A retry with the same key and the same body does not send again. Broadcast replays the stored response with `Idempotency-Replayed: true`.
3. The same key with a different body fails with `422`.
4. A retry while the first is still processing gets `409`. Wait and retry.

Keys are scoped to the token and expire after 24 hours.

### Same key, same body

- Nothing is sent again
- Stored response comes back
- `Idempotency-Replayed: true`

### Same key, different body

- Fails with `422`

### First request still processing

- Gets `409`
- Wait and retry

What happens when a request arrives with a key Broadcast has already seen.

## Statuses, and when mail waits

A transactional email moves through **Pending**, **Queued** and **Sent**, or ends as **Failed**. The **Transactional** page lists every send, with filters by recipient and status, and lets you resend one.

1. **Pending**
2. **Queued**
3. **Sent**

An email that cannot be sent ends as Failed.

![A transactional email panel with its status, recipient, details and a timeline, beside an email preview and a Resend Email button](https://sendbroadcast.net/assets/docs/transactional-emails/transactional-emails-show-04cd0db4.png)

Click a send to see its timeline, a preview and the Resend Email button.

Transactional email has the highest priority in Broadcast, and one worker thread is kept for it alone. But it still respects the email server's hourly limit. If the server is full, the email stays **Queued** and Broadcast retries every minute. After 30 minutes, every page of the channel shows a notice. After 24 hours it is marked **Failed**: "Email server at hourly limit: no room for 24 hours, giving up".

The fix is in your setup, not your code. Send transactional email through its own email server, one that does not send broadcasts. Then a large newsletter can never delay a login code. See [How sending works](/docs/how-sending-works).

## Double opt-in from your own email

If you want your own confirmation email design, send a transactional with `double_opt_in: true` and put `{{ confirmation_url }}` in the body. Broadcast creates the confirmation and fills in the link. The [Transactional Email API](/docs/api-transactional-email) lists the other Liquid variables, such as `{{ first_name }}`.

## What you learned

- Transactional email bypasses unsubscribe status. Use it only for real service messages.
- `POST /api/v1/transactionals.json` with `to`, and `subject` and `body` or a `template_id`.
- An `Idempotency-Key` makes retries safe for 24 hours.
- Give transactional email its own email server, so broadcasts never delay it.

## Quiz
1. Does a transactional email reach someone who unsubscribed from your marketing?
   - No, unsubscribe blocks all email
   - Yes, transactional email bypasses unsubscribe status (correct)
   - Only if you pass include_unsubscribe_link
   Transactional email is delivered even to unsubscribed recipients. Redacted subscribers and the global suppression list still block it.
2. Your client times out and retries a receipt. How do you stop a second copy going out?
   - Wait 24 hours before you retry
   - Send the retry with a different subject
   - Delete the first email from the dashboard
   - Send the same Idempotency-Key header on both requests (correct)
   A retry with the same key and body replays the stored response and does not send again.
3. A transactional email waits because the server is at its hourly limit. When does Broadcast give up?
   - After 24 hours, and it marks the email Failed (correct)
   - After 1 minute
   - Never, it waits forever
   Broadcast retries every minute. After 24 hours with no room, it marks the email Failed.
