Store your contacts once, unique by email, and group them into audiences you target from a broadcast. A typed property registry keeps your data clean, and a 1,000-row batch upsert gets a list in fast.
import { BirdClient } from "@messagebird/sdk";
import { render } from "@react-email/render";
import { WelcomeEmail } from "./emails/welcome";
const bird = new BirdClient({ apiKey: process.env.BIRD_API_KEY! });
const { data, error } = await bird.email.send({
from: "Bird <hello@bird.com>",
to: ["ada@example.com"],
subject: "Your invite is ready",
html: await render(<WelcomeEmail name="Ada" />),
}).safe();
if (error) throw error;
console.log(data.id);
// → "em_2bX91Yk8h..."You can sign in any time at bird.com/login.
Your test API key is on your dashboard, ready to send.
One contact record. Attributes that don't drift.
Audiences are part of the Bird Email API: a contact is unique by email and shared across your audiences, an audience is just a set of those contacts, and attributes always live on the contact, not copied onto every list. The property registry means a typo'd field is rejected at write time, not discovered in a campaign.
Five things that keep your data clean.
The data model does the bookkeeping, so your lists don't rot.
- 01
One record per person.
One contact per email, with first and last name, an external ID, and your own custom data.
- 02
Audiences are membership.
An audience is a set of contacts you add and remove freely. Attributes always live on the contact, never copied onto the list.
- 03
Typed property registry.
Define your custom fields up front: string, number, or boolean, with optional fallbacks. Unknown keys and wrong-typed values are rejected at write time.
- 04
Batch upsert up to 1,000.
Upsert up to 1,000 contacts in a single call, and join them to an audience in the same request.
- 05
Exact-match lookup.
Find a contact by exact email match, so your own systems can sync without scanning every record.
Get a whole list in with one call.
Upsert is idempotent on email, so re-running an import updates rather than duplicates. Add up to 1,000 contacts and join them to an audience in the same request, with no separate membership step and no de-dupe pass afterward. Larger lists go up in batches of 1,000. Each call is independent, so an interrupted import is safe to resume where it stopped.
// Upsert up to 1,000 contacts and add them to an audience at once.
await bird.email.contacts.batchUpsert({
audienceId: "aud_2bX91Yk8h",
contacts: [
{ email: "ada@example.com", firstName: "Ada", data: { plan: "growth" } },
{ email: "grace@example.com", firstName: "Grace", data: { plan: "free" } },
],
});Fields that can't drift.
Custom contact data goes through a typed registry. Declare each field once as a string, number, or boolean, with an optional fallback, and a typo'd key or wrong-typed value is rejected the moment you write it, not discovered halfway through a campaign. Because attributes live on the contact, not the list, moving someone between audiences never leaves a stale copy behind.
Audiences & contacts FAQ
Are contacts per-audience or global?+
What is the property registry?+
How do I import a list?+
How do audiences relate to broadcasts?+
The rest of the Email platform
One API, one set of keys. Explore the other capabilities.
About 40% of the world's commercial email already runs on Bird.
Transactional and marketing email on infrastructure we've run for a decade. Audiences are one capability of the Bird Email API: sending, broadcasts, deliverability, suppression, and analytics ship with it.