# Send transactional email

Send one email from a template or inline content with an org API key, safely retryable with an idempotency key.

Send a receipt, a shipping notice or any one-off email from your backend with an
**org API key**. The send goes through the same guards as the console: a working
sender and the suppression list. It is transactional mail, so it does not count
against your plan's monthly email quota, which counts marketing mail; it has
its own limits instead: a daily number of API sends set by your plan, and 30
sends to one address an hour.

> **Note:**
  Create an API role under **Settings > API roles** with `emails_api_sends`
  (create, read), then an API key with that role under **Settings > API keys**.
  Make a template under **Messaging > Templates**.

The `emails` calls ship in `@buildbase/sdk` 1.0.0. If `bb.emails` is `undefined`, you are on an earlier version.

## Quick example

```typescript title="lib/email.ts"
import { BuildBaseApiError, createServerClient } from '@buildbase/sdk/server';

const bb = createServerClient({
  serverUrl: 'https://api.console.buildbase.app',
  apiToken: process.env.BUILDBASE_API_TOKEN!, // <orgId>:<secret>
});

export async function sendShippedEmail(order: {
  id: string;
  email: string;
  name: string;
}) {
  const { id } = await bb.emails.send(
    {
      to: { email: order.email, name: order.name },
      templateId: process.env.SHIPPED_TEMPLATE_ID!,
      data: { orderId: order.id },
    },
    { idempotencyKey: `order-${order.id}-shipped` }
  );
  return id;
}
```

`{{orderId}}` in the template becomes `order.id`. `{{name}}` and `{{email}}` are
always filled from `to`.

## Inline content

Send without a template by giving a `subject` and `html`, `text` or both. Text on its
own also becomes the HTML part.

```typescript title="lib/email.ts"
await bb.emails.send({
  to: { email: 'ada@example.com' },
  subject: 'Your export is ready',
  text: 'Download it from your dashboard within 24 hours.',
});
```

## Parameters

| Field        | Type                                                  | Default                | Description                                    |
| ------------ | ----------------------------------------------------- | ---------------------- | ---------------------------------------------- |
| `to`         | `{ email: string; name?: string }`                    | Required               | The recipient                                  |
| `templateId` | `string`                                              | -                      | A template's id; or give `subject` with a body |
| `subject`    | `string`                                              | The template's subject | Overrides the template's subject               |
| `html`       | `string`                                              | -                      | Inline HTML body                               |
| `text`       | `string`                                              | -                      | Inline plain-text body                         |
| `data`       | `Record<string, string \| number \| boolean \| null>` | `{}`                   | Merge-tag values, up to 100 keys               |
| `senderId`   | `string`                                              | The default sender     | Send from a specific sender                    |

The second argument takes `idempotencyKey` and `signal`.

## Retries and idempotency

The SDK never retries a send for you. To retry safely, pass an `idempotencyKey` and
retry with the same key:

- A repeat of a key in the next 24 hours answers with the first send's `id` and
  `replayed: true`, and sends nothing.
- The same key with a different body is refused with `IDEMPOTENCY_KEY_REUSED`.
- A repeat that arrives while the first is still in progress gets
  `IDEMPOTENCY_IN_PROGRESS`; retry shortly.

Derive the key from the unit of work (`order-1234-shipped`), not a fresh value per
attempt.

## Checking delivery

`send` resolves once the email is queued. Read its status with `emails.get`:

```typescript title="lib/email.ts"
const message = await bb.emails.get(id);
// message.status: 'pending' | 'sent' | 'failed' | 'bounced' | 'skipped' | 'complained'
```

## Handling refusals

A refusal throws `BuildBaseApiError` with the HTTP `status` and a `code`. Nothing is
sent and nothing is counted against your quota.

```typescript title="lib/email.ts"
try {
  await sendShippedEmail(order);
} catch (err) {
  if (err instanceof BuildBaseApiError && err.code === 'RECIPIENT_SUPPRESSED') {
    // They unsubscribed, or the address bounced. Do not retry.
  } else {
    throw err;
  }
}
```

| Code                   | Status | Meaning                                                                                                 |
| ---------------------- | ------ | ------------------------------------------------------------------------------------------------------- |
| `RECIPIENT_SUPPRESSED` | 422    | The address bounced, is a spam trap, or unsubscribed                                                    |
| `SEND_LIMIT_REACHED`   | 429    | Too many API sends to that address this hour, or for the org today                                      |
| `UNKNOWN_REFERENCE`    | 422    | No template with that id                                                                                |
| `NO_SENDER_CONFIGURED` | 409    | The organization has no working email sender                                                            |
| `API_KEY_REQUIRED`     | 403    | Called with a console session instead of an API key                                                     |
| `SENDING_SUSPENDED`    | 403    | BuildBase has suspended the organization's sending; nothing is queued. Contact support                  |
| `API_KEYS_SUSPENDED`   | 403    | BuildBase has suspended the organization's API keys; every request with one is refused. Contact support |

Going over either API send limit is the `429` `SEND_LIMIT_REACHED` above; the
monthly email quota never refuses this call. A key whose role lacks
`emails_api_sends` gets a `401`.

## Next steps

- [Manage subscribers](/server-sdk/subscribers) from your backend
- [Templates and merge tags](/email/templates-and-merge-tags)
- [Server SDK overview](/server-sdk/overview)
