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
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.
await bb.emails.send({
to: { email: '[email protected]' },
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
idandreplayed: 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:
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.
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 from your backend
- Templates and merge tags
- Server SDK overview