Documentation
Sign inGet started

Contacts

A contact is a stored recipient: an email address plus whatever you know about the person behind it. Contacts hold your recipient data so you can group people into audiences and reuse them.
Manage contacts under Contacts → All contacts in the dashboard, with bird contacts from the terminal, through the contacts API, or with any of the SDKs.

Reaching contacts

To email one person, send to their address with the send API; the contact record keeps who they are, ready to reuse. To reach many at once, send a batch, or group them into an audience and send a broadcast. Storing a contact sends nothing on its own.

The Contacts page

The Contacts page lists everyone in your workspace with their email, name, external ID, and creation date. Search by email address to find one, click a row to open the contact, and use the header buttons to add a single contact or import many. Viewing needs the email_marketing read permission; adding, editing, and deleting need write.
The Contacts page in the dashboard, listing stored contacts by email, name, external ID, and creation date, with search and Properties, Import, and Add contact buttons

What a contact holds

Every contact has a workspace-unique email address and, optionally, a name and your own identifier for it:
FieldWhat it is
emailThe address, required and unique per workspace. We store it trimmed and lowercased, so Sam@Acme.com and sam@acme.com are the same contact.
first_nameOptional given name, used to personalize a send.
last_nameOptional family name.
external_idOptional. Your own primary key for the person (a user ID from your database), unique per workspace when set. It is how you match a contact back to your own records without relying on the email.
dataCustom property values, one per registered contact property.
channelsRead-only. The channels the contact can be reached on, derived from the identifiers it holds, so a contact with an address includes email. A value you do not recognize is a channel we added after you wrote your code, so handle it rather than treating it as an error.
Each contact also has an ID with a con_ prefix and its created and updated timestamps. The full field contract is in the API reference.

Contact properties

Contact properties are the typed schema for the custom fields on a contact. Register a property once per workspace, and from then on every contact can have a value for it under data. Declaring the schema up front is what makes personalization and segmentation reliable: a value always arrives as the type you declared, so a template or a filter can count on it.
The Contact properties page in the dashboard, listing six properties with their key, type, fallback value, and creation date, one of them showing an Archived badge
Manage them under Contacts → Contact properties, reached from the sidebar or the Properties button on the Contacts page. Each property has a key, a type, and an optional fallback:
  • Key is the name you reference the value by, plan_tier for example. It must be lowercase and start with a letter (^[a-z][a-z0-9_]*$), and it is fixed once created.
  • Type is one of string, number, boolean, or datetime, and it is also fixed once created. A datetime takes an RFC 3339 timestamp with an explicit offset such as 2026-01-15T11:30:00+02:00, which we normalize to UTC at second precision, so that value is stored and returned as 2026-01-15T09:30:00Z. A bare date with no time is rejected. The dashboard labels these Text, Number, True / false, and Date & time.
  • Fallback value is what a contact with no value of its own reads as, so a missing plan_tier can come out as free rather than a blank.
Properties are archived rather than deleted. Archiving stops new writes to the key while keeping every value already stored, and the key stays reserved so it can never come back as a different type. Unarchive it to bring it back. That reservation is also why the type is immutable: a stored number must never start being read as a string. A workspace can register up to 200 properties, and archived ones count toward that cap because their keys are still held.
Set property values wherever you edit a contact. The dashboard's contact form shows one typed input per active property, and the CLI and API take the same keys under data.

Importing and syncing contacts

The dashboard import is the fastest way to load a list. From Import on the Contacts page, drop in a CSV, TSV or Excel file of up to 50,000 contacts, one per row, with a header row naming the columns. A CSV can be up to 50 MB; a spreadsheet up to 10 MB, because it has to be read whole rather than streamed.
The header row is read to propose which contact field each column is, so a column called "Email Address", "E-Mail" or "Correo electrónico" all land on the email field, and a single column holding a whole name is split into a first and a last name. Where two columns could fill the same field, the one whose values back up its name wins. Each column shows a few of its own values so you can see what it holds, and a name being split is shown against the value it came from. Change any of it from the dropdown beside each column. Everyone in the file can be added to one or more audiences as part of the same import.
Each row is matched to an existing contact by the identifiers it carries and updated, or created if it is new, so re-importing the same file is an upsert rather than a pile of duplicates. Before anything is written you are told how many of the first rows cannot be imported as mapped, and when the run finishes every skipped row is listed with the line number to open in your own file and what was wrong with it.
To sync from your own database, script the CLI or call the batch endpoint. bird contacts create <email> adds one; bird contacts batch upserts up to 1,000 in a single call, which is how you keep your contact list in step with your system: one batch per run rather than one request per person.
const contact = await bird.contacts.create({
  email: "jane@acme.com",
  first_name: "Jane",
});
console.log(contact.id); // "con_…"
Each batch entry is matched automatically on every identifier it has (email address, phone number, or external ID), and the optional match_on field forces matching on a single one of those instead. An entry can also set custom property values, and can drop every contact in the request straight into audiences via audience_ids. Each entry succeeds or fails on its own, and the response reports one result per entry in submission order:
Ejemplo de código
{
  "data": [
    {
      "contact_id": "con_01ky7q5t51echr7mqj5c08423b",
      "entry": { "email": "alex@example.com", "phone_number": null, "external_id": null },
      "matched_on": "email",
      "status": "updated"
    },
    {
      "contact_id": "con_01ky7q6mxdfhe86c9dqyt866pz",
      "entry": { "email": "jamie@example.com", "phone_number": null, "external_id": null },
      "matched_on": null,
      "status": "created"
    },
    {
      "contact_id": "con_01ky7q7rv9e9pt4vkr0w0gxq5e",
      "entry": { "email": "casey@example.com", "phone_number": null, "external_id": "user_2214" },
      "matched_on": "external_id",
      "status": "updated"
    }
  ]
}
Two defaults worth knowing for a sync. A batch merges data keys onto what the contact already stores, so an import touching one attribute never wipes the others; send a key with a null value to clear that one key, or data_mode: "replace" to overwrite the whole map. And set your own external_id on every contact, so a later sync finds the same person even after their email changes. That is the third entry above: user_2214 already exists, so the entry resolves to that contact and writes the new email in place.

Deleting a contact

Deleting a contact is permanent: the record and its audience memberships go, and nothing recovers them. Suppressions are untouched, though. An address that unsubscribed or hard-bounced stays on your suppression list after you delete the contact, so deleting someone never quietly makes them mailable again.

Next steps

  • Audiences: group contacts into reusable lists
  • Suppressions: the workspace list of addresses we will not deliver to, kept separate from your contacts
  • Batch sending: reaching many recipients in one call, up to 100 messages per request
  • CLI: scripting contacts, properties, and audiences with the bird command
  • API reference: full request and response schemas