# Contacts

Keep one record per person across your messaging channels, with tags, notes and subscription state, and use it to target broadcasts and sequences.

A **contact** is a person you talk to. One contact can have several **channels**: their Instagram DMs, their WhatsApp number and their Telegram chat can all point at the same record. Contacts carry tags, notes, a subscription flag and [custom fields](https://pozy.ai/docs/inbox/custom-fields), and they are what [broadcasts](https://pozy.ai/docs/automations/broadcasts) and [sequences](https://pozy.ai/docs/automations/sequences) target.

Contacts belong to a profile. Pozy creates them as people message your accounts; you can also create and import them yourself.

## Create a contact

Create the record and, optionally, its first channel in the same call by passing `accountId`, `platform` and `platformIdentifier` (the person's id on that platform; a phone number for WhatsApp and SMS).

```bash cURL
curl -X POST https://api.pozy.ai/v1/contacts \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "profileId": "6650c1e2a3b4c5d6e7f80911",
    "name": "Marta Pons",
    "email": "marta@example.com",
    "tags": ["vip", "newsletter"],
    "accountId": "6650c1e2a3b4c5d6e7f80a44",
    "platform": "whatsapp",
    "platformIdentifier": "+34600111222"
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/contacts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    profileId: "6650c1e2a3b4c5d6e7f80911",
    name: "Marta Pons",
    email: "marta@example.com",
    tags: ["vip", "newsletter"],
    accountId: "6650c1e2a3b4c5d6e7f80a44",
    platform: "whatsapp",
    platformIdentifier: "+34600111222",
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.contact.id, data.channel?.id);
```
```python Python
import os
import requests

resp = requests.post(
    "https://api.pozy.ai/v1/contacts",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "profileId": "6650c1e2a3b4c5d6e7f80911",
        "name": "Marta Pons",
        "email": "marta@example.com",
        "tags": ["vip", "newsletter"],
        "accountId": "6650c1e2a3b4c5d6e7f80a44",
        "platform": "whatsapp",
        "platformIdentifier": "+34600111222",
    },
    timeout=30,
)
print(resp.json()["contact"]["id"])
```

`isSubscribed` defaults to `true`. If something about the request was off but the contact was still created (for example a channel that could not be attached), the response carries a `warning`.

## Import in bulk

`POST /v1/contacts/bulk` imports up to 1,000 contacts per call into one profile. Duplicates are skipped and any new tags are merged onto the existing contact.

```json
{
  "profileId": "6650c1e2a3b4c5d6e7f80911",
  "accountId": "6650c1e2a3b4c5d6e7f80a44",
  "contacts": [
    { "name": "Marta Pons", "platformIdentifier": "+34600111222", "tags": ["spring-2026"] },
    { "name": "Leo Martin", "platformIdentifier": "+34600333444", "email": "leo@example.com" }
  ]
}
```

```json
{ "success": true, "created": 2, "skipped": 0, "errors": [], "total": 2 }
```

- `accountId` is required whenever contacts carry a `platformIdentifier`, and then every row needs one.
- On WhatsApp and SMS, identifiers are normalised to digits. A value that is not a phone number is rejected for that row only.
- Bad rows are reported in `errors` with HTTP `200`; the rest of the import still goes through. Check `errors` on every call.

## Find contacts

`GET /v1/contacts` lists a profile's contacts. Filter with `search` (name, email and company), `tag` or `tags`, `platform`, `accountId` and `isSubscribed`, and page with `limit` (up to 200) and `skip`.

Each contact includes its tags, custom field values, notes, message counters (`messagesSentCount`, `messagesReceivedCount`) and the last time a message went each way. `GET /v1/contacts/{id}/channels` lists every channel of one contact, with the conversation id for each.

## Update and tag

`PATCH /v1/contacts/{contactId}` changes only the fields you send: `name`, `email`, `company`, `avatarUrl`, `tags`, `isSubscribed`, `isBlocked` and `notes`.

`tags` replaces the whole list. To add one tag, read the contact, append, and send the full list back. Workflows can tag contacts for you with their `add_tag` and `remove_tag` nodes; see [Workflow nodes](https://pozy.ai/docs/automations/workflow-nodes).

## Opt-in and blocking

| Field | Effect |
| --- | --- |
| `isSubscribed: false` | Broadcast segments that filter on `isSubscribed: true` leave the contact out, and sequences with `exitOnUnsubscribe` (on by default) end for them |
| `isBlocked: true` | Marks the person as blocked in your records |

Respect opt-outs on every channel. For WhatsApp, SMS and RCS, the platforms and carriers require prior consent before you send promotional messages. See [WhatsApp](https://pozy.ai/docs/whatsapp) and [SMS](https://pozy.ai/docs/telephony/sms).

## Delete

`DELETE /v1/contacts/{contactId}` permanently deletes the contact and all its channels. It cannot be undone.

## Contact webhooks

| Event | Fires when |
| --- | --- |
| `contact.updated` | A contact's details changed |
| `contact.deleted` | A contact was deleted |
| `contact.tag_added`, `contact.tag_removed` | A tag was added or removed, by the API, a workflow node or a tracked link click in a comment automation |
| `contact.field_changed` | A custom field changed value |

Tag and field events fire once per real change. Re-adding a tag the contact already has, or writing a field's current value, fires nothing. Creating a contact with tags fires nothing either; only changes to existing contacts do. The payload's `source` says who made the change: `api` (the API or the dashboard), `workflow` or `automation`.

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/contacts`](https://pozy.ai/docs/api/listContacts) | List contacts |
| [`POST /v1/contacts`](https://pozy.ai/docs/api/createContact) | Create contact |
| [`GET /v1/contacts/{contactId}`](https://pozy.ai/docs/api/getContact) | Get contact |
| [`PATCH /v1/contacts/{contactId}`](https://pozy.ai/docs/api/updateContact) | Update contact |
| [`DELETE /v1/contacts/{contactId}`](https://pozy.ai/docs/api/deleteContact) | Delete contact |
| [`GET /v1/contacts/{contactId}/channels`](https://pozy.ai/docs/api/getContactChannels) | List channels for a contact |
| [`POST /v1/contacts/bulk`](https://pozy.ai/docs/api/bulkCreateContacts) | Bulk create contacts |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`contact.updated`](https://pozy.ai/docs/webhooks/events#contact-updated) | Contact updated. |
| [`contact.deleted`](https://pozy.ai/docs/webhooks/events#contact-deleted) | Contact deleted. |
| [`contact.tag_added`](https://pozy.ai/docs/webhooks/events#contact-tag-added) | Contact tag added. |
| [`contact.tag_removed`](https://pozy.ai/docs/webhooks/events#contact-tag-removed) | Contact tag removed. |
| [`contact.field_changed`](https://pozy.ai/docs/webhooks/events#contact-field-changed) | Contact field changed. |
