# Contacts API

Contacts are the people you email. All routes need a bearer API key (see [API Overview](/docs/api/overview)).

## The contact object

```json
{
  "id": "clx...",
  "email": "ada@example.com",
  "firstName": "Ada",
  "lastName": "Lovelace",
  "tags": ["customer", "vip"],
  "status": "SUBSCRIBED",
  "createdAt": "2026-10-04T12:00:00.000Z"
}
```

`status` is one of `PENDING`, `SUBSCRIBED`, `UNSUBSCRIBED` or `BOUNCED`. Only `SUBSCRIBED` contacts receive campaigns. See [Double Opt-In](/docs/concepts/double-opt-in).

Limits: a contact can have at most 50 tags, and each tag at most 100 characters.

## List contacts

```text
GET /api/v1/contacts
```

| Query    | Meaning                                                                             |
| -------- | ----------------------------------------------------------------------------------- |
| `limit`  | Page size, default 20, maximum 100                                                  |
| `after`  | Cursor from the previous page                                                       |
| `status` | One of `PENDING`, `SUBSCRIBED`, `UNSUBSCRIBED`, `BOUNCED`. Anything else is a `422` |
| `tag`    | Only contacts that have this exact tag                                              |
| `q`      | Case-insensitive search in email, first name and last name                          |

Results are newest first.

## Create or update a contact

```text
POST /api/v1/contacts
```

```json
{
  "email": "ada@example.com",
  "firstName": "Ada",
  "lastName": "Lovelace",
  "tags": ["customer"],
  "status": "SUBSCRIBED"
}
```

- `email` is required. It is trimmed and lowercased.
- `status` defaults to `SUBSCRIBED`. An unrecognized value also falls back to `SUBSCRIBED`.
- This is an upsert on the email address. A new contact returns `201`. An existing contact is updated and returns `200`.
- On an existing contact, the `tags` you send replace its tags (an omitted `tags` becomes an empty list). Always send the fields you want to keep.
- A request can always lower a contact's status (to `UNSUBSCRIBED` or `BOUNCED`), but it never raises someone who unsubscribed back to `SUBSCRIBED` on its own say-so. That takes `"consent": "explicit"`, which records that the person themselves agreed (it is logged with the source `api`). `"consent": "declined"` records a refusal. Any other `consent` value is a `422`. A `BOUNCED` contact is never raised, and a `SUBSCRIBED` contact is never moved back to `PENDING`. The same rule applies to re-adding an address that was deleted after opting out.
- Setting a contact to `UNSUBSCRIBED` or `BOUNCED` adds the address to the suppression list and removes the contact from every list. Setting it back to `SUBSCRIBED` later does not lift the suppression, so a suppressed address is still never mailed.
- A contact saved as `SUBSCRIBED` starts any live automation whose trigger is a new subscriber.

## Get a contact

```text
GET /api/v1/contacts/:id
```

Returns the contact object, or `404`.

## Update a contact

```text
PATCH /api/v1/contacts/:id
```

Send only the fields to change: `email`, `firstName`, `lastName`, `tags`, `status`.

- A new `email` that belongs to another contact in your workspace returns `409` with code `CONFLICT`.
- An invalid email, an invalid `status`, a `tags` value that is not an array, or too many or too long tags returns `422`.
- An empty `firstName` or `lastName` clears the field.
- `tags` replaces the whole tag list. Adding a tag that the contact did not have starts any live automation triggered by that tag.
- `UNSUBSCRIBED` and `BOUNCED` behave as in the create route above.

## Delete a contact

```text
DELETE /api/v1/contacts/:id
```

Returns `204`. The contact is removed permanently.

## Lists a contact is on

```text
GET /api/v1/contacts/:id/lists
```

Returns an array in `data` (it is not paginated). Each item has the list's `id`, `name` and `createdAt`, plus `addedAt`, the time the contact joined it.

To add or remove a contact from a list, use the [Lists API](/docs/api/lists).

## Example

```bash
curl -X POST https://flomailr.com/api/v1/contacts \
  -H "Authorization: Bearer $FLOMAILR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@example.com","firstName":"Ada","tags":["customer"]}'
```
