> This page is for Omni Channel.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.hams.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.hams.ai/_mcp/server.

# Contacts

> Find, read, create, and update the contacts behind your conversations, also by phone number.

A contact is the person on the other side of a conversation. The send endpoint creates a contact when you send to a phone number that has no contact. Most integrations therefore only read contacts. Create and update contacts directly when your own system is the main record of your customers.

All the endpoints on this page are under `/api/v1/accounts/{account_id}/developer/contacts`. They take the same `api_access_token` header as the send endpoint. The contact `id` in the path is the id that the API returns in `id` and `contact_id`.

## Find a contact

```
POST /api/v1/accounts/{account_id}/developer/contacts/filter
```

Each item in `payload` is 1 condition on a contact attribute. `query_operator` joins the item to the next item. Leave it out on the last item.

```bash
curl -X POST https://omni-channel.hams.ai/api/v1/accounts/1/developer/contacts/filter \
  -H "api_access_token: $HAMS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "payload": [
      { "attribute_key": "phone_number", "filter_operator": "equal_to", "values": ["+971500000000"] }
    ]
  }'
```

```json
{
  "meta": { "count": 1, "current_page": 1 },
  "payload": [
    {
      "id": 42,
      "name": "Katherine Johnson",
      "email": "katherine@example.com",
      "phone_number": "+971500000000",
      "identifier": "cust_9931",
      "custom_attributes": { "plan": "pro" }
    }
  ]
}
```

| `attribute_key`                  | Operators                                                                 |
| -------------------------------- | ------------------------------------------------------------------------- |
| `name`, `email`                  | `equal_to`, `not_equal_to`, `contains`, `does_not_contain`                |
| `phone_number`                   | `equal_to`, `not_equal_to`, `contains`, `does_not_contain`, `starts_with` |
| `identifier`, `country_code`     | `equal_to`, `not_equal_to`                                                |
| `city`, `company_name`           | `equal_to`, `not_equal_to`, `contains`, `does_not_contain`                |
| `labels`                         | `equal_to`, `not_equal_to`, `is_present`, `is_not_present`                |
| `created_at`, `last_activity_at` | `is_greater_than`, `is_less_than`, `days_before`                          |
| `blocked`                        | `equal_to`, `not_equal_to`                                                |

A contact custom attribute can also be an `attribute_key`. A key or an operator that the API does not know returns `422`. The result has 20 contacts on each page. Add `?page=2` for the next page.

## Read a contact by phone number

```
GET /api/v1/accounts/{account_id}/developer/contacts/by_phone?phone_number=%2B971500000000
```

The `phone_number` query parameter is the phone number of the contact in E.164 format. The API adds the `+` if it is not there. A `+` in a query string reads as a space, so encode it as `%2B`.

```json
{
  "payload": {
    "id": 42,
    "name": "Katherine Johnson",
    "email": "katherine@example.com",
    "phone_number": "+971500000000",
    "identifier": "cust_9931",
    "blocked": false,
    "custom_attributes": { "plan": "pro" },
    "additional_attributes": {},
    "notes": [
      { "id": 7, "content": "Prefers calls after 2 PM.", "created_at": 1790000000 }
    ]
  }
}
```

The `payload` has the same fields as [Read a contact](#read-a-contact), without `contact_inboxes`. It also has `notes`: the 10 newest notes of the contact, newest first. Each note has `id`, `content`, and `created_at` as a Unix timestamp in seconds.

If no contact in the account has the number, the API returns `404`. If `phone_number` is missing, the API returns `422` with the code `phone_number_required`. If the number is not valid, the API returns `422` with the code `invalid_phone_number`.

## Create or update a contact by phone number

```
POST /api/v1/accounts/{account_id}/developer/contacts/create_or_update
```

Use this endpoint to keep a contact in sync with your own system. The phone number identifies the contact. If the account has a contact with the number, the API updates it. If not, the API creates a contact. You do not need to find the contact first.

```json
{
  "phone_number": "+971500000000",
  "name": "Katherine Johnson",
  "email": "katherine@example.com",
  "identifier": "cust_9931",
  "custom_attributes": { "plan": "enterprise" },
  "notes": ["Upgraded to Enterprise."]
}
```

The request must have `phone_number` in E.164 format. The optional fields are `name`, `email`, `identifier`, `blocked`, `custom_attributes`, `additional_attributes`, `notes`, and `avatar_url`. The API changes only the fields that you send.

* `custom_attributes` and `additional_attributes` merge into the stored values, as in [Update a contact](#update-a-contact).
* `notes` is a list of text values. The API adds each value as a new note on the contact. It never changes or deletes a note that exists.
* `avatar_url` is the URL of an image. The API downloads the image in the background and sets it as the avatar of the contact.

The API does the whole request as 1 unit. If a field is not valid, the API changes nothing. A blank note is not valid. Two requests for the same new number at the same time create 1 contact.

```json
{
  "payload": {
    "id": 42,
    "name": "Katherine Johnson",
    "phone_number": "+971500000000",
    "custom_attributes": { "plan": "enterprise" },
    "notes": [
      { "id": 8, "content": "Upgraded to Enterprise.", "created_at": 1790000500 },
      { "id": 7, "content": "Prefers calls after 2 PM.", "created_at": 1790000000 }
    ]
  },
  "created": false
}
```

The `payload` has the same shape as in [Read a contact by phone number](#read-a-contact-by-phone-number). `created` is `true` and the status is `201` when the API created the contact. `created` is `false` and the status is `200` when the API updated a contact.

| Status | Code                    | Cause                                                                                                                                                        |
| ------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `422`  | `phone_number_required` | The request has no `phone_number`.                                                                                                                           |
| `422`  | `invalid_phone_number`  | The number is not in E.164 format.                                                                                                                           |
| `422`  | `validation_failed`     | A field is not valid. For example, another contact has the email address or the identifier. A blank note also causes this error. `message` gives the reason. |

## Read a contact

```
GET /api/v1/accounts/{account_id}/developer/contacts/{id}
```

The response has the contact in `payload`. It includes `contact_inboxes`, the inboxes where the contact has an address.

## Create a contact

```
POST /api/v1/accounts/{account_id}/developer/contacts
```

```json
{
  "name": "Katherine Johnson",
  "email": "katherine@example.com",
  "phone_number": "+971500000000",
  "identifier": "cust_9931",
  "custom_attributes": { "plan": "pro" }
}
```

The fields are `name`, `email`, `phone_number`, `identifier`, `custom_attributes`, `additional_attributes`, `blocked`, and `avatar_url`. `phone_number` must be in E.164 format. The response has the contact in `payload.contact`.

`email`, `phone_number`, and `identifier` are each unique in the account. A value that another contact has already fails with `422`. Set `identifier` to the key of the customer in your own system. Then you can find the same contact later, also after the customer changes a phone number.

A new contact has no conversation. To start a conversation, send a message with `contact_id`. See [Messages](/omni-channel/api/messages).

## Update a contact

```
PATCH /api/v1/accounts/{account_id}/developer/contacts/{id}
```

The API changes only the fields that you send. `custom_attributes` and `additional_attributes` merge into the stored values. A key that you send replaces the same key. A key that you do not send stays.

```json
{ "custom_attributes": { "plan": "enterprise", "renewal": "2027-01-31" } }
```

## List contacts

```
GET /api/v1/accounts/{account_id}/developer/contacts?page=1
```

Each page has 20 contacts. The list includes only contacts with an email address, a phone number, or an identifier. The count can therefore be smaller than the total in the dashboard. To show only contacts with a label, add `labels[]=vip`.

The developer API cannot delete a contact. An administrator deletes contacts in the dashboard.

## Related

* [Messages](/omni-channel/api/messages)
* [Conversations](/omni-channel/api/conversations)
* [Pre-call context](/omni-channel/api/precall-context)
* [Errors](/omni-channel/api/errors)