# Contacts: the people your agent knows, with custom attributes

> Keep one record per person for each agent: verified visitors, leads, CSV imports and API contacts with custom attributes that server-side actions can read as {{contact.*}}.

A contact is a person your agent knows: their email, phone, name, their user id on your site, and up to 50 custom attributes of your own, such as a plan or an account manager. Each agent keeps its own contacts, on the **Leads** tab under **Contacts**. [Server-side API actions](/en/docs/api-actions) can read them as `{{contact.*}}` placeholders. The AI itself never sees the attributes.

## Where contacts come from

| Source | Shown as | What creates or changes the contact |
| --- | --- | --- |
| [Identity verification](/en/docs/identity-verification) | **Verified visitor** | A visitor your site signed in chats with the agent. The contact gets their user id as **External ID**, the name and email your page passed, and metadata as attributes. |
| [Lead form](/en/docs/lead-collection) or [contact details written in the chat](/en/docs/lead-alerts-and-export#save-contact-details-written-in-the-chat) | **Lead** | A lead is saved with an email or phone number that no contact has yet. |
| [CSV import](#import-a-csv-file) | **Import** | A row of your file. |
| [REST API](/en/docs/rest-api#contacts) or [MCP server](/en/docs/mcp-server) | **API** | Your own systems create or update the contact. |

There is one contact per external ID and one per email address. Each source changes a contact only as far as it can be trusted:

- **Verified visitors.** The contact is found by the user id only. When the name, email or metadata your page passes changes, the contact follows: metadata keys that are valid attribute names (lower-case, see [Attributes](#attributes)) become attributes, and other keys are left out. The email is taken only if no other contact has it. A verified visitor is never merged into an existing contact by email, because only the user id is signed: otherwise a signed-in visitor could claim someone else's contact by passing their email. The **Last seen** time follows their chats.
- **Leads.** A new email or phone number creates a contact. If the email or phone number already belongs to a contact that also came from a lead, its empty fields are filled in. A contact from a verified visitor, an import or the API is never changed by a lead, because anyone can type any email address. Visitors are never told whether a contact already existed.
- **Imports, the API and the dashboard.** These come from you, so their values replace what is stored.

Leads stay in **Collected Leads** as before. A contact is an extra record next to them, not a replacement.

## Find and open a contact

The **Contacts** card lists the agent's contacts, most recently seen first, with the number used out of your plan's limit. Search by email, name, external ID or phone, or filter by source. Click a contact to open it:

- Its email, phone, name and external ID, and when it was first and last seen.
- All its attributes.
- Its conversations, up to the 20 most recent, each linking to the **Conversations** tab. These are the chats the contact was linked to, and for a verified visitor every chat with their user id.

## Attributes

Attributes are your own fields on a contact, such as `plan`, `account_manager` or `seats`.

- A name starts with a lower-case letter and uses lower-case letters, digits and underscores, up to 40 characters.
- `email`, `name`, `phone`, `external_id`, `id`, `source`, `first_seen_at`, `last_seen_at`, `created_at` and `updated_at` are reserved.
- A value is text up to 1,000 characters, a number or true/false. Through the dashboard and a CSV file, values are saved as text.
- A contact can have up to 50 attributes.

To edit them, open a contact, change the names and values, use **Add attribute** or the **×** beside one to remove it, and click **Save attributes**.

## Import a CSV file

1. On the **Contacts** card, click **Import CSV** and choose a `.csv` file of up to 5 MB and 10,000 rows. Commas, semicolons and tabs all work as separators.
2. intoCHAT reads the file and shows a preview without saving anything: how each column is used, the first rows, and how many contacts would be created, updated and skipped, with the reasons for the first 20 skipped rows.
3. Click **Import** to save. The result shows how many contacts were created, updated and skipped.

How the file is read:

- The columns `external_id`, `email`, `phone` and `name` fill in the contact. Column names are turned into snake_case first, so `External ID` and `externalId` both work.
- Every other column becomes an attribute: `Plan Tier` is saved as `plan_tier`. A column name that can't become one, such as `2024 spend`, stops the import with a message naming it.
- The columns `source`, `first_seen_at`, `last_seen_at`, `created_at`, `updated_at` and `id` are ignored, so an exported file can be imported again.
- Each row updates the contact with its `external_id`, otherwise the contact with its `email`, otherwise it creates a new contact. A row whose email belongs to a contact with a different external ID is skipped.
- An empty cell changes nothing. To remove an attribute, edit the contact.
- A row without an `external_id` or an `email` is skipped, and so is a row with an invalid email or phone number, a value over 1,000 characters, or one that would give a contact more than 50 attributes.
- New contacts stop at your plan's limit; the rows beyond it are skipped with that reason.

## Export a CSV file

**Export CSV** downloads the contacts that match the current search and filter: `external_id`, `email`, `phone`, `name`, `source`, `first_seen_at`, `last_seen_at`, then one column per attribute. A cell that starts with `=`, `+`, `-` or `@` gets a leading apostrophe, so a spreadsheet app shows it as text and never runs it as a formula. Importing the file again removes the apostrophe.

## Use contacts in actions

A [server-side API action](/en/docs/api-actions) can use these placeholders in its URL, parameters, headers and body:

| Placeholder | Filled with |
| --- | --- |
| `{{contact.email}}` | The contact's email |
| `{{contact.name}}` | The contact's name |
| `{{contact.phone}}` | The contact's phone number |
| `{{contact.external_id}}` | The contact's external ID |
| `{{contact.<attribute>}}` | One attribute, for example `{{contact.plan}}` |

```text
GET https://api.example.com/accounts/{{contact.external_id}}?plan={{contact.plan}}
```

Which contact is used:

- For a visitor your site signed in and verified, the contact with their user id as external ID, and no other.
- For anyone else, the contact of the lead saved in this conversation, found by its email or phone number. A contact with an external ID is never used this way: it belongs to a user of your site, and only their verified chats can read it.

intoCHAT fills these in on its servers. The AI never sees them and can't choose them. If there is no contact, or the contact doesn't have one of the values the action uses, the action doesn't run. The agent is told the action needs details it doesn't have, in the same words whether or not a contact exists, so the visitor learns nothing about your contacts. Unlike `{{user.*}}`, nothing is ever sent empty.

- An unknown placeholder, such as `{{contact.Plan}}` or `{{contact.id}}`, can't be saved.
- **Send test request** in the editor has no visitor, so an action that uses `{{contact.*}}` can't be tested there.
- A [client-side action](/en/docs/client-actions) never gets contact data, because its request is built in the visitor's browser.
- In the Playground, **Test as a signed-in visitor** uses the contact with that user ID, if you have one. Test identities never create contacts.

> Without identity verification, a contact is matched by the email or phone number the visitor typed, which anyone can type. Only put things in attributes that you'd be comfortable with that visitor's action seeing, or let the action look up anything sensitive by `{{contact.external_id}}` for verified visitors.

## Delete a contact

Open the contact and click **Delete contact**. Its attributes go with it. Its leads and conversations stay. If the same verified visitor chats again, or the same email comes in as a new lead, a new contact is created. Deleting an agent deletes its contacts. [Duplicating an agent](/en/docs/quick-start) doesn't copy them.

## Who can do what

Everyone on your [team](/en/docs/team) can see contacts and export them. Editors, admins and the owner can edit attributes, import a file and delete contacts.

## Limits

| | Free | Starter | Pro | Enterprise |
| --- | --- | --- | --- | --- |
| Contacts per agent | 100 | 5,000 | 25,000 | 100,000 |

An import or an API call that would go over the limit is refused with a message naming the limit. New verified visitors and leads beyond it are simply not saved as contacts, and the chat isn't affected. After a downgrade, an agent keeps all its contacts and can still update them, but gets no new ones until it is under the limit.
