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

# Contacts

> Create, find and update contacts, and manage their handles, notes, consent and merges.

A contact is one customer. A contact has a name, owner, tags, custom fields and a list of handles (`identities`): a
phone number, an email address or an account on a channel. Conversations, notes and consent attach to the contact.

All routes are under `https://api.impellabs.tech/api/v1`. Send `Authorization: Bearer imp_...` on each request. See
[Authentication](https://docs.impellabs.tech/api/authentication.md) and [Conventions](https://docs.impellabs.tech/api/conventions.md). The API reference lists every field.

## Who can see a contact

A token acts as the person who made it. A contact answers `404` with `There is no contact like that.` when it does not
exist or when the person may not see it. A contact has a `visibility`: `workspace` (everyone) or `owner` (its owner
and their managers).

| Action | Who |
| - | - |
| Find, create, update, merge, add notes | Any person who sees the contact. |
| Remove a contact | Its owner, an owner or an admin. Others get `403`. |
| Create, change or remove a contact field | An owner or an admin. Others get `403`. |

A removed contact is archived, not deleted. Its handles become free for a new contact.

## List contacts

`GET /contacts` returns a page: `{ results, count, page, page_size, pages }`. `page_size` is 20 unless you send
`page_size` (1 to 100).

| Query | Meaning |
| - | - |
| `q` | Search by name or handle. |
| `tag` | A tag. |
| `owner` | A member id, or `me`. |
| `channel` | A channel kind the contact has a handle on. |
| `group` | A contact group name, or `none`. |
| `source` | The channel the contact first came on, or a lead source kind. |
| `anonymous` | `true` to include visitors with no name. |
| `sort` | `name` or `updated` (the default). |
| `order` | `asc` or `desc` (the default). |
| `page`, `page_size` | The page. |

```bash theme={null}
curl "https://api.impellabs.tech/api/v1/contacts?q=dana&sort=name&order=asc" \
  -H "Authorization: Bearer imp_..."
```

## Create a contact

`POST /contacts`. Send a `name`, a `phone`, an `email`, or any of them. You must send at least one. Write a phone
number with its country code.

| Field | Meaning |
| - | - |
| `name` | Up to 255 characters. |
| `phone`, `email` | Saved as handles. A handle that is already on another contact becomes a suggested merge. |
| `owner_id` | A member id. The default owner is the person who makes the contact. |
| `visibility` | `workspace` or `owner`. |
| `language` | Up to 16 characters. |
| `time_zone` | A time zone name. |
| `tags` | A list of tags, 30 at most, 40 characters each. |
| `custom_fields` | An object of field key and value. Each key must be a contact field of the workspace. |
| `do_not_contact` | `true` or `false`. |

```bash theme={null}
curl -X POST "https://api.impellabs.tech/api/v1/contacts" \
  -H "Authorization: Bearer imp_..." \
  -H "Content-Type: application/json" \
  -d '{"name": "Dana", "phone": "+15550100", "email": "dana@example.org", "tags": ["wholesale"]}'
```

The reply is `201`:

```json theme={null}
{
  "id": "c91e5b0a-2d6f-4f0e-8c1d-0a7c7b6f3e21",
  "name": "Dana",
  "owner_id": "a2c4f1d0-6b3e-4a7d-9e52-1c0b8d7e6f10",
  "visibility": "workspace",
  "language": "",
  "time_zone": "",
  "tags": ["wholesale"],
  "custom_fields": {},
  "do_not_contact": false,
  "anonymous": false,
  "first_channel": "",
  "identities": [
    { "id": "1b2c...", "kind": "phone", "value": "+15550100", "verified": false, "source": "person",
      "created_at": "2026-01-12T09:30:00+00:00" },
    { "id": "3d4e...", "kind": "email", "value": "dana@example.org", "verified": false, "source": "person",
      "created_at": "2026-01-12T09:30:00+00:00" }
  ],
  "merged_ids": [],
  "created_at": "2026-01-12T09:30:00+00:00",
  "updated_at": "2026-01-12T09:30:00+00:00"
}
```

| Status | Cause |
| - | - |
| `400` | No name, phone or email; a phone number without a country code; an email that is not valid; a custom field key that is not a contact field; a value of the wrong type. |

## Read, update and remove

* `GET /contacts/{contact_id}` returns the contact with `consent`, `timeline`, `suggestions` (the number of suggested
  merges), `crm_lead` and `app_records`.
* `PATCH /contacts/{contact_id}` changes only the fields you send. The editable fields are `name`, `owner_id`,
  `visibility`, `language`, `time_zone`, `tags`, `custom_fields`, `do_not_contact`. Set a custom field to `null` to
  remove its value.
* `DELETE /contacts/{contact_id}` answers `204`.

```bash theme={null}
curl -X PATCH "https://api.impellabs.tech/api/v1/contacts/c91e5b0a-2d6f-4f0e-8c1d-0a7c7b6f3e21" \
  -H "Authorization: Bearer imp_..." \
  -H "Content-Type: application/json" \
  -d '{"tags": ["wholesale", "priority"], "do_not_contact": false}'
```

`GET /contacts/{contact_id}/timeline` returns the contact's history. Use `limit` (1 to 100), `before` (the time from
`next`) and `kind` (one kind, or kinds separated by commas; `note` returns the notes).

## Handles

* `POST /contacts/{contact_id}/identities` with `{ "kind", "value" }` adds a handle, for example `phone`, `whatsapp`,
  `email` or `instagram`. A typed handle is not verified. `201` returns `{ id, kind, value, verified }`.
  If another contact holds the value, the reply is `409` and ImpelLabs adds a suggested merge.
* `DELETE /contacts/{contact_id}/identities/{identity_id}` removes a handle. `404` if the contact has no such handle.

## Notes

* `GET /contacts/{contact_id}/notes` returns the notes you may see, newest first.
* `POST /contacts/{contact_id}/notes` with `{ "text", "visibility"?, "department_id"? }`. `text` has 5000
  characters at most. `visibility` is `everyone` (the default), `department` or `private`. A `department` note needs
  `department_id`, one of your departments.
* `PATCH` and `DELETE /contacts/{contact_id}/notes/{note_id}` work only for the person who wrote the note. Others get
  `404`.

```bash theme={null}
curl -X POST "https://api.impellabs.tech/api/v1/contacts/c91e5b0a-2d6f-4f0e-8c1d-0a7c7b6f3e21/notes" \
  -H "Authorization: Bearer imp_..." \
  -H "Content-Type: application/json" \
  -d '{"text": "Prefers a call after 14:00.", "visibility": "private"}'
```

## Consent

`POST /contacts/{contact_id}/consent` records a decision of the customer.

| Field | Meaning |
| - | - |
| `channel` | A channel kind such as `whatsapp` or `email`, or `any`. |
| `status` | `granted` or `withdrawn`. |
| `purpose` | `marketing` (the default) or `service`. |
| `text` | Optional words that show where the decision came from. |

The reply is `201` with `{ "consent": [ { "channel", "purpose", "status", "source", "at" } ] }`: the newest decision
for each channel and purpose. The source is `staff`.

```bash theme={null}
curl -X POST "https://api.impellabs.tech/api/v1/contacts/c91e5b0a-2d6f-4f0e-8c1d-0a7c7b6f3e21/consent" \
  -H "Authorization: Bearer imp_..." \
  -H "Content-Type: application/json" \
  -d '{"channel": "email", "status": "granted", "purpose": "marketing"}'
```

## Merge

Two contacts can be one person. ImpelLabs suggests merges when handles or names match.

* `GET /contacts/matches` lists suggestions. `state` is `review` (the default) or `merged`. `contact` limits the list
  to one contact.
* `POST /contacts/matches/{match_id}/accept` merges the two contacts. `reject` dismisses the suggestion. `undo` splits
  a merge that was made.
* `POST /contacts/{contact_id}/merge` with `{ "other_id" }` merges two contacts you name. The reply is `201` with the
  match. A merge that is not allowed answers `400` with `detail`.

## Contact fields

A contact field is a custom field of the workspace, such as a customer number.

* `GET /contacts/fields` lists the fields.
* `POST /contacts/fields` creates one: `key` (small letters, numbers and `_`, starting with a letter), `label`, `kind`
  (default `text`), and optionally `required`, `choices`, `value_map`, `help_text` and `order`. A `select` or
  `multiselect` field needs `choices`.
* `PATCH` and `DELETE /contacts/fields/{field_id}` change and archive a field.

All three changes need an owner or an admin.

## Import contacts from a sheet

An import loads a CSV or Excel file. You send the file first, then check the column mapping, then start the job.

1. `POST /files` with a multipart `file`. The reply has the file `id`.
2. `POST /imports/groups` with `{ "file_id", "group", "mapping"? }`. The `mapping` holds column names:
   `email`, and optionally `name`, `company` and `consent`. ImpelLabs suggests columns from the headers. Each person in the file gets the tag
   `group:<name>`. The reply is `201` with the import, the file `headers`, sample rows and the suggested columns.
3. `POST /imports/{import_id}/check` with `{ "mapping": { "group": "<name>", "email": "<column>", "name": "<column>" } }`
   saves the mapping and runs a dry run. Nothing is written. The mapping always names the `group` and the `email`
   column. `name`, `company` and `consent` are optional columns.
4. `POST /imports/{import_id}/start` starts the job. It answers `202`. A large import can wait for an approval first;
   the import `status` shows `waiting`.
5. `GET /imports/{import_id}` shows `status` and the `rows` counts. `GET /imports/{import_id}/errors` pages the rows
   that had a problem.
6. `POST /imports/{import_id}/undo` reverses the import inside its undo window. Outside it, the reply is `409`.

```bash theme={null}
curl -X POST "https://api.impellabs.tech/api/v1/files" \
  -H "Authorization: Bearer imp_..." \
  -F "file=@people.csv"

curl -X POST "https://api.impellabs.tech/api/v1/imports/groups" \
  -H "Authorization: Bearer imp_..." \
  -H "Content-Type: application/json" \
  -d '{"file_id": "9a3e...", "group": "Spring list", "mapping": {"email": "Email", "name": "Name"}}'
```

A row without a valid email is left out and reported in the errors. A person is on the list for marketing email only
when the file has a yes or no column that you map as `consent`, or when consent exists already. The same file again
changes nothing. You can read only the imports you made.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.