> ## Documentation Index
> Fetch the complete documentation index at: https://docs.instantcampaign.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Contacts & lists

> Upsert contacts by email, look them up, and manage list membership from your own systems.

A contact belongs to one workspace and is unique by email. Emails are trimmed and lower-cased before use. Lists are subscription lists, and a contact can be on many of them.

| Endpoint                          | Scope            | What it does                                                         |
| --------------------------------- | ---------------- | -------------------------------------------------------------------- |
| `GET /v1/contacts`                | `contacts:read`  | Page through contacts, newest first. Filter by `status` or `listId`. |
| `POST /v1/contacts`               | `contacts:write` | Create or update a contact (upsert on email)                         |
| `GET /v1/contacts/search?email=`  | `contacts:read`  | One contact with custom field values and lists, or `null`            |
| `GET /v1/lists`                   | `contacts:read`  | Every list with its membership count                                 |
| `POST /v1/lists/{id}/subscribe`   | `contacts:write` | Add a contact to a list, creating the contact if needed              |
| `POST /v1/lists/{id}/unsubscribe` | `contacts:write` | Remove a contact from a list                                         |

## Upserting a contact

```bash theme={null}
curl -X POST https://instantcampaign.ai/api/v1/contacts \
  -H "Authorization: Bearer ic_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane@example.com",
    "firstName": "Jane",
    "country": "GB",
    "language": "en",
    "customFields": { "plan": "pro" }
  }'
```

The response is `201` with `created: true` for a new contact and `200` with `created: false` for an update. Any field you leave out is left as it is.

* **Subscription status is never changed by an upsert.** Use the list endpoints for membership.
* `customFields` are written only for keys defined under **Settings → Custom Fields**. Unknown keys are ignored without an error.
* `source` and `metadata` are written only when the contact is created.
* When the plan's contact limit is reached, creating a *new* contact answers `403` with `code: "contact_limit_reached"`. Updating an existing contact still works.

## List membership

`POST /v1/lists/{id}/subscribe` creates the contact if it does not exist, with status `ACTIVE` and source `api`. It then makes the membership `ACTIVE`, and a membership that was unsubscribed becomes active again. The call also fires the `CONTACT_ADDED_TO_LIST` webhook and starts journeys that trigger on joining the list.

<Warning>
  This endpoint sends no double opt-in confirmation, whatever the list's double opt-in setting. Subscribing someone here is a statement that you already have their consent.
</Warning>

`POST /v1/lists/{id}/unsubscribe` marks the membership `UNSUBSCRIBED` (it isn't deleted, so the opt-out is remembered). The contact and its other lists are untouched. `removed: false` means there was nothing to unsubscribe.

`contactCount` on `GET /v1/lists` counts every membership on the list, including pending and unsubscribed ones.
