Skip to content
BuildBase

Manage subscribers

Create, update, unsubscribe and delete email subscribers by address from your backend with an org API key.

Keep your email audience in step with your app: add a subscriber when someone signs up, tag them as their plan changes, and unsubscribe them when they ask. Subscribers are addressed by email and are the same contacts you see in the console audience.

Before you start

Create an API role under Settings > API roles with audience (read, create, update, delete), plus audience_lists (update) to add people to lists. Then an API key with that role under Settings > API keys. Tags are created in the console.

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

Quick example

lib/audience.ts
import { 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 onSignup(user: {
  email: string;
  name: string;
  plan: string;
}) {
  const subscriber = await bb.subscribers.upsert({
    email: user.email,
    name: user.name,
    tags: ['customer'],
    attributes: { plan: user.plan },
    lists: [process.env.NEWSLETTER_LIST_ID!],
  });
  return subscriber.created; // true the first time, false after
}

Upsert

upsert creates the subscriber, or updates the one with that address. What you leave out is kept.

FieldTypeDescription
emailstringRequired. Case does not matter
namestringDisplay name; a new subscriber without one uses the email
tagsstring[]Tag slugs to add, up to 50
removeTagsstring[]Tag slugs to remove
attributesRecord<string, string | number | boolean | null>Merged over the current attributes, up to 100 keys
listsstring[]Static list ids to add them to
timezone, language, country, currencystringProfile fields

An upsert never changes whether someone is subscribed. A sign-up form that upserts again does not resubscribe a person who left.

An unknown tag slug or list id is refused with UNKNOWN_REFERENCE and changes nothing. A new subscriber counts toward your plan's contact limit; updating one never does.

Read, unsubscribe and resubscribe

lib/audience.ts
const subscriber = await bb.subscribers.get('[email protected]'); // null when there is none

await bb.subscribers.unsubscribe('[email protected]', {
  reason: 'asked by email',
});
await bb.subscribers.resubscribe('[email protected]');

unsubscribe stops marketing and transactional sends to the address; account email such as password resets still arrives. resubscribe is refused with RECIPIENT_SUPPRESSED for an address that bounced or is a spam trap.

Delete

lib/audience.ts
await bb.subscribers.delete('[email protected]');

Delete takes the subscriber off every list (firing audience.removed_from_list for each) and then soft-deletes them: they no longer appear in the console, in reads or in any send, but the record is kept, marked deleted, and so is their history, such as the email already sent to them, so a delete is not an erasure of their data. The console has no delete; this call is the only way to remove a contact. The address can be added again later and then starts subscribed, so to stop email to a person who asked, unsubscribe them instead.

Events

Every change fires the same events as the console, so your workflows and webhooks see it: audience.member_created, audience.member_updated, audience.tag_added, audience.tag_removed, audience.attribute_changed, audience.added_to_list, audience.removed_from_list (on delete), audience.unsubscribed and audience.resubscribed.

Handling refusals

Refusals throw BuildBaseApiError with status and code:

CodeStatusMeaning
UNKNOWN_REFERENCE422A tag slug or list id that does not exist; see field
RECIPIENT_SUPPRESSED422resubscribe for an address that bounced
SUBSCRIBER_NOT_FOUND404No subscriber with that address
API_KEY_REQUIRED403Called with a console session instead of an API key

The contact or list-size limit is a 402 with the plan's code.

Next steps