Skip to content
BuildBase

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.

Before you start

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

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.

lib/email.ts
await bb.emails.send({
  to: { email: '[email protected]' },
  subject: 'Your export is ready',
  text: 'Download it from your dashboard within 24 hours.',
});

Parameters

FieldTypeDefaultDescription
to{ email: string; name?: string }RequiredThe recipient
templateIdstring-A template's id; or give subject with a body
subjectstringThe template's subjectOverrides the template's subject
htmlstring-Inline HTML body
textstring-Inline plain-text body
dataRecord<string, string | number | boolean | null>{}Merge-tag values, up to 100 keys
senderIdstringThe default senderSend 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:

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.

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;
  }
}
CodeStatusMeaning
RECIPIENT_SUPPRESSED422The address bounced, is a spam trap, or unsubscribed
SEND_LIMIT_REACHED429Too many API sends to that address this hour, or for the org today
UNKNOWN_REFERENCE422No template with that id
NO_SENDER_CONFIGURED409The organization has no working email sender
API_KEY_REQUIRED403Called with a console session instead of an API key
SENDING_SUSPENDED403BuildBase has suspended the organization's sending; nothing is queued. Contact support
API_KEYS_SUSPENDED403BuildBase 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