Contacts
Store, enrich, and keep track of the people you email
A contact is Bitelio's record of a single email recipient: a unique identifier tied to an email address.
Adding contacts
There are several routes for getting contacts into a Bitelio project:
- Via
/v1/track— if you track an event against an email that isn't in the project yet, the contact is created on the fly (and starts out subscribed). - Via
POST /contacts— creates or upserts one contact. If the email already exists it's updated rather than rejected; the response carries a_meta: { isNew, isUpdate }block, with status201when the contact was created and200when it was updated. - Bulk CSV import through
POST /contacts/import— send a CSV (≤ 5 MB), then track the job withGET /contacts/import/:jobId.emailmust be the first column; every remaining column becomes a key underdata.*. - Bulk subscribe / unsubscribe / delete through
POST /contacts/bulk-subscribe,bulk-unsubscribe, andbulk-delete— each takes at most 1,000 contact IDs and hands back a job ID you can poll atGET /contacts/bulk/:jobId. - Pushing emails into a static segment using
POST /segments/:id/memberswithcreateMissing: true, which creates any addresses not yet in the project. - By hand, from the dashboard.
Contact Data
Arbitrary key-value pairs can be attached to any contact, and that data then feeds segmentation and template personalization.
Data types
For each key in a project, Bitelio infers the type from the first non-null value it encounters:
| Type | Description |
|---|---|
| String | Text that doesn't parse as a date. |
| Number | Integers and floats alike. |
| Boolean | true or false. |
| Date | Strings shaped like YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS[.sss]Z. Segment operators that understand dates require the full ISO 8601 form. |
Type inference happens once per project, at first write. So if a key gets typed as number, later string values are still saved — but the segment filter UI keeps treating the key as a number. Mixed up the types by accident? The cleanest fix is DELETE /contacts/fields/:field to drop the field entirely, then re-import it with consistent values.
Default data type
Anything that isn't recognised as a number, a boolean, or an ISO 8601 date falls back to being a string.
Non-persistent values
Wrapping a value as { value, persistent: false } makes it available for a single render without ever writing it to the contact:
{
"email": "user@example.com",
"data": {
"firstName": "Ada",
"resetCode": { "value": "ABC123", "persistent": false }
}
}Here firstName gets stored on the contact, while resetCode exists only for this one send and is thrown away afterwards. That makes it the right tool for ephemeral secrets — password reset codes, magic links, one-time tokens.
Special value handling
A few values receive special treatment when a contact is created or updated:
| Value | Behavior | Example |
|---|---|---|
Empty string ("") | Skipped — nothing is stored or changed | { name: "" } → Field is skipped |
null | Deletes the field from the contact's data | { name: null } → Field is deleted |
| Other values | Written normally | { name: "John" } → Stored as "John" |
Removing contact data
Pass null for a field when creating or updating a contact to erase it. An empty string won't do the same thing —
empty strings are filtered out before the write, so existing data survives them.
Reserved keys
A handful of keys belong to Bitelio and live on the contact itself rather than in data. They're readable, but can't be written through data:
| Key | Description |
|---|---|
email | The contact's email address |
createdAt | When the contact was created |
updatedAt | When the contact was last modified |
subscribed | Whether the contact currently receives marketing email (boolean) |
On top of that, these keys are silently stripped from any data payload — including them in /v1/track, POST /contacts, or PATCH /contacts/:id neither stores them nor produces an error:
id, bitelio_id, bitelio_email, unsubscribeUrl, subscribeUrl, manageUrl
The final three are generated fresh for each recipient at send time and surface as template variables (see Templates).
Special keys
| Key | Description |
|---|---|
| locale | The contact's preferred language as an ISO 639 code (e.g. 'en', 'fr', 'es'). When set, it takes precedence over the project-wide locale on contact-facing pages and in email footers |
Subscription State
Each contact carries a subscribed flag governing which categories of email reach them. Contacts created through /v1/track start subscribed; with POST /contacts the flag takes whatever you send (defaulting to false).
On updates, leaving subscribed out preserves whatever it currently is — omission and false are different things. Send an explicit true or false whenever you intend a change.
Whenever subscribed changes, Bitelio records an event on the contact automatically:
- flip to
true→ acontact.subscribedevent - flip to
false→ acontact.unsubscribedevent
This happens no matter what caused the flip — a dashboard edit, the public unsubscribe page, a bulk job, or an automatic unsubscribe following a bounce or spam complaint. Workflows can branch on both events.
How contacts become unsubscribed
There are several paths to the unsubscribed state:
- Manual — someone toggles it in the dashboard or through the API.
- Self-service — the contact clicks an unsubscribe link in an email, landing on the hosted public unsubscribe page.
- Automatic on hard bounce — a permanent bounce unsubscribes the contact; soft bounces leave the state untouched.
- Automatic on complaint — the contact reports your email as spam and their mailbox provider relays the complaint to Bitelio.
Emails by subscription state
Whether marketing email is delivered depends on the subscription flag; transactional email always goes through, subscribed or not.
| Email type | Subscribed | Unsubscribed |
|---|---|---|
| Transactional (via /v1/send) | Delivered | Delivered |
| Campaigns (marketing) | Delivered | Not delivered |
| Campaigns (headless) | Delivered | Not delivered |
| Campaigns (transactional) | Delivered | Delivered |
| Automations (marketing template) | Delivered | Not delivered |
| Automations (headless template) | Delivered | Not delivered |
| Automations (transactional template) | Delivered | Delivered |
Transactional emails and marketing templates
Going through the transactional endpoint (/v1/send) doesn't let a marketing template reach an unsubscribed contact —
the subscription check still applies. If the message has to arrive regardless of opt-out, switch to a transactional template.