# Contacts (/concepts/contacts)

Contacts in Plunk represent an individual email recipient. Each contact has an identifier and is linked to an email address.

## Adding contacts

Contacts can be added to your Plunk project in several ways:

* Using [`/v1/track`](/api-reference/public-api/trackEvent) — tracking an event for a contact that doesn't exist yet automatically creates it (subscribed by default).
* Using [`POST /contacts`](/api-reference/contacts/createContact) — create or upsert a single contact. Existing emails are **updated**, not rejected; the response includes a `_meta: { isNew, isUpdate }` block and uses status `201` for new contacts and `200` for updates.
* Bulk **CSV import** via `POST /contacts/import` — upload a CSV (≤ 5 MB) and poll `GET /contacts/import/:jobId` for status. The first column must be `email`; any other columns map to keys under `data.*`.
* Bulk **subscribe / unsubscribe / delete** via `POST /contacts/bulk-subscribe`, `bulk-unsubscribe`, and `bulk-delete` — each accepts up to 1,000 contact IDs and returns a job ID; poll `GET /contacts/bulk/:jobId` for progress.
* Adding emails to a **static segment** with `POST /segments/:id/members` and `createMissing: true` will create any contacts that don't already exist.
* Manually through the dashboard.

## Contact Data

You can associate custom data with each contact using key-value pairs. This data can be used for segmentation and personalization.

### Data types

Contact data types are inferred from the **first non-null value** Plunk sees for a given key in your project:

| Type    | Description                                                                                                                              |
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| String  | Any text value that doesn't look like a date.                                                                                            |
| Number  | Numeric values, including integers and floats.                                                                                           |
| Boolean | `true` or `false`.                                                                                                                       |
| Date    | A string matching `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS[.sss]Z`. The full ISO 8601 form is required for date-aware operators in segments. |

The type is sampled per-project on first write — once a key is typed as `number`, subsequent string values for that key are still stored, but the **inferred type** stays `number` for the segment filter UI. If you accidentally mix types, the safest reset is to delete the field via `DELETE /contacts/fields/:field` and re-import with the type you want.

<Callout title="Default data type" type="info">
  If a value doesn't match number, boolean, or the ISO 8601 date format, it's typed as a string.
</Callout>

### Non-persistent values

Send a value as `{ value, persistent: false }` to use it once for template rendering without storing it on the contact:

```json
{
  "email": "user@example.com",
  "data": {
    "firstName": "Ada",
    "resetCode": { "value": "ABC123", "persistent": false }
  }
}
```

`firstName` is saved on the contact; `resetCode` is available to the template for this send only and is then discarded. Use this for one-shot values like password reset codes, magic links, and one-time tokens.

### Special value handling

When creating or updating contacts, certain values are handled specially:

| Value               | Behavior                                    | Example                               |
| ------------------- | ------------------------------------------- | ------------------------------------- |
| Empty string (`""`) | Ignored - field is not stored or updated    | `{ name: "" }` → Field is skipped     |
| `null`              | Delete - field is removed from contact data | `{ name: null }` → Field is deleted   |
| Other values        | Stored/updated normally                     | `{ name: "John" }` → Stored as "John" |

<Callout title="Removing contact data" type="info">
  To remove a field from a contact's data, set it to `null` when creating or updating the contact. Empty strings are
  automatically filtered out and won't overwrite existing data.
</Callout>

### Reserved keys

Some keys are reserved by Plunk and managed at the contact level (not inside `data`). You can read them but you can't set them through `data`:

| Key          | Description                                                              |
| ------------ | ------------------------------------------------------------------------ |
| `email`      | The contact's email address                                              |
| `createdAt`  | Timestamp of when the contact was created                                |
| `updatedAt`  | Timestamp of the last update                                             |
| `subscribed` | Boolean indicating whether the contact is subscribed to marketing emails |

The following keys are also **silently filtered out** of any `data` payload — sending them through `/v1/track`, `POST /contacts`, or `PATCH /contacts/:id` will not store them and will not return an error:

`id`, `plunk_id`, `plunk_email`, `unsubscribeUrl`, `subscribeUrl`, `manageUrl`

The last three are auto-generated per-recipient at send time and exposed as template variables (see [Templates](/concepts/templates)).

### Special keys

| Key    | Description                                                                                                                                                                                  |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| locale | The contact's preferred locale in ISO 639 (e.g. 'en', 'fr', 'es'). Specifying the locale field on a contact will override the project-wide locale for contact-facing pages and email footers |

## Subscription State

Every contact has a `subscribed` field that determines which types of emails they will receive. A newly created contact is subscribed by default when created via `/v1/track`; contacts created via `POST /contacts` use whatever value you pass (default: `false`).

When you update a contact, **omitting `subscribed` keeps the current state** — it is not the same as passing `false`. To change the state, pass an explicit `true` or `false`.

Every flip of `subscribed` automatically tracks an event on the contact:

* `subscribed` flipped to `true` → `contact.subscribed` event
* `subscribed` flipped to `false` → `contact.unsubscribed` event

These events fire on every path that changes subscription state — manual edits, the public unsubscribe page, bulk operations, automatic unsubscribes from bounces / complaints. You can branch on them in workflows.

### How contacts become unsubscribed

A contact can become unsubscribed in several ways:

* **Manually** through the dashboard or via the API.
* **Self-service** by clicking the unsubscribe link in an email (powered by the public unsubscribe pages).
* **Automatically** on a permanent (hard) email bounce. Soft bounces don't change subscription state.
* **Automatically** when a recipient marks one of your emails as spam (their mailbox provider reports the complaint back to Plunk).

### Emails by subscription state

The subscription state controls whether a contact receives marketing emails. Transactional emails are always delivered regardless of subscription state.

| Email type                                                                           | Subscribed | Unsubscribed  |
| ------------------------------------------------------------------------------------ | ---------- | ------------- |
| **Transactional** (via [/v1/send](/api-reference/public-api/sendTransactionalEmail)) | 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">
  Even when using the transactional API endpoint (`/v1/send`), you cannot send a marketing template to an unsubscribed
  contact. Use a transactional template instead if the email must reach unsubscribed contacts.
</Callout>

## What's next

<Cards>
  <Card title="Custom fields" href="/guides/custom-fields">
    How to set, type, and clean up arbitrary contact data.
  </Card>

  <Card title="Importing from CSV" href="/guides/importing-contacts">
    Bulk-load contacts and their custom fields.
  </Card>

  <Card title="Segments" href="/concepts/segments">
    Group contacts dynamically or statically for targeting.
  </Card>

  <Card title="Unsubscribe pages" href="/guides/unsubscribe-pages">
    Hosted unsubscribe / preferences pages and the URL template variables.
  </Card>
</Cards>
