# Contacts (/concepts/contacts)

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`](/api-reference/public-api/trackEvent) — 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`](/api-reference/contacts/createContact) — 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 status `201` when the contact was created and `200` when it was updated.
* Bulk **CSV import** through `POST /contacts/import` — send a CSV (≤ 5 MB), then track the job with `GET /contacts/import/:jobId`. `email` must be the first column; every remaining column becomes a key under `data.*`.
* Bulk **subscribe / unsubscribe / delete** through `POST /contacts/bulk-subscribe`, `bulk-unsubscribe`, and `bulk-delete` — each takes at most 1,000 contact IDs and hands back a job ID you can poll at `GET /contacts/bulk/:jobId`.
* Pushing emails into a **static segment** using `POST /segments/:id/members` with `createMissing: 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.

<Callout title="Default data type" type="info">
  Anything that isn't recognised as a number, a boolean, or an ISO 8601 date falls back to being a string.
</Callout>

### Non-persistent values

Wrapping a value as `{ value, persistent: false }` makes it available for a single render without ever writing it to the contact:

```json
{
  "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" |

<Callout title="Removing contact data" type="info">
  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.
</Callout>

### 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](/concepts/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` → a `contact.subscribed` event
* flip to `false` → a `contact.unsubscribed` event

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](/api-reference/public-api/sendEmail)) | 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     |

<Callout title="Transactional emails and marketing templates" type="warn">
  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.
</Callout>

## What's next

<Cards>
  <Card title="Custom fields" href="/guides/custom-fields">
    Setting, typing, and tidying arbitrary data on contacts.
  </Card>

  <Card title="Importing from CSV" href="/guides/importing-contacts">
    Load contacts and their fields in bulk.
  </Card>

  <Card title="Segments" href="/concepts/segments">
    Organise contacts into dynamic or static groups for targeting.
  </Card>

  <Card title="Unsubscribe pages" href="/guides/unsubscribe-pages">
    The hosted unsubscribe and preferences pages, and their URL variables.
  </Card>
</Cards>
