# Contacts (/docs/agents/contacts)

> For the complete documentation index, see [llms.txt](https://docs.tandemcx.ai/llms.txt). Any page on this site can be fetched as Markdown by appending `.md` to its URL.

<Callout type="info">
    **This page is being rolled out.** If **Contacts** isn't in your sidebar, it isn't enabled for
    your organization yet — contact support.
</Callout>

## What a contact is

A contact is one record for one person your organization talks to. It holds:

- **A name** — a display name, or a first and last name. A contact with neither is listed by
  one of its identifiers instead.
- **Identifiers** — the phone numbers, email addresses, chat visitor ids, and external ids
  the person is recognized by. A contact with no identifiers can never be matched to a
  conversation.
- **Tags** — up to 50 short labels, 40 characters each, for your own grouping.
- **Notes** — free text, up to 4,000 characters.
- **Attributes** — a value for each field your organization has defined. See
  [Attributes](#attributes) below.
- **Linked records** — the person's record in your Zendesk, once your organization has a Zendesk
  integration. See [Linking a contact to Zendesk](#linking-a-contact-to-zendesk).

Names, tags, notes, and text attributes keep what you type, in any language or script. Tandem
removes control characters and the invisible characters that override text direction, and turns
a line break in a name or tag into a space.

Open **Contacts → All Contacts** to see the list, and click **Edit** on a row to open the contact
in a sidebar. Everything in the sidebar is edited behind one **Save**: names, tags, notes,
attribute values, and identifiers added or removed all go up together.

You create a contact from **Add Contact**, by [importing a CSV file](#importing-contacts), or
through the API, and a voice agent can create one for you. With **Recognize callers** on, a call
matches the caller's number to a contact and creates one holding that number when nothing
matches, and a returning website chat visitor is matched to a contact the same way. Each of
those moves the contact's **Conversations** count and its **Last Contact**. Nothing is on until
somebody turns that switch on for an agent — see
[Contacts on an agent](/docs/agents/voice-agents/contacts).

A visitor arriving at your chat widget for the first time is matched to nothing and no contact is
created for them, because the match runs on an id the widget left in their browser on an earlier
visit. Text messages are matched to contacts only when **Recognize texters** is on; see
[Text messages](#text-messages).

Adding a contact by hand asks for a first name, a last name, a phone number, an email
address, and tags. Any one of the first four is enough to save. The dialog deliberately asks
for no attribute values; you fill those in on the contact itself.

### Finding a contact

The list is ordered by **Last Contact**, most recent first, and pages with **Previous** and
**Next** rather than by page number: the service returns a forward cursor and no total, so
there is no count to jump to a page against. There is no sort control, because sorting the
page in hand would misrepresent the rest of the list.

The search box above the list takes a name, a number, or an email address, and how it runs
depends on what you type:

- A value that **looks like an email address or a phone number** is matched against
  identifiers as a prefix, so a partial number finds every contact whose number starts with
  it.
- Anything else is matched against **names and tags** as a substring, ignoring case, across
  every contact in your organization. A contact created or renamed a moment ago can take a short
  time to turn up in a name search. Searching by its phone number or email address finds it at
  once.

A name or tag search pages with **Previous** and **Next** like the list. A phone or email search
returns one page.

## A contact's conversations

The contact's sidebar lists its conversations under **Conversations**, newest first, on every
channel. That includes calls to an IVR workflow from one of the contact's phone numbers, which
are matched by number because a workflow does not look the caller up. Each row shows the
channel, the start time in UTC, the duration, who handled the conversation, and its disposition
when it has one. Click a row to open the conversation in Call Logs. **Load More** fetches older
ones.

A workflow with **Redact Data in Logging** on (on the **General** tab of its settings) records no
phone number for its conversations, so its calls and texts are not listed here. A conversation
that reaches a voice or chat agent with **Recognize callers** on is still listed when the agent
matches it to the contact.

A call matched only by number does not move the contact's **Conversations** count or its
**Last Contact** in the list, so a contact can show conversations in its sidebar and a count of
zero. The **Activity** section above **Conversations** shows when the contact was first and last
seen, and is left out for a contact no conversation has recognized yet.

### From Call Logs

In **Insights → Call Logs**, the **Source** column shows the contact's name above the number for
a call from one of your contacts. Click the name to open that contact's sidebar on the
**Contacts** page.

## Text messages

With **Recognize texters** on, the texts your workflows send are matched to a contact by the
other person's phone number. Texts your phone numbers receive are matched once inbound texting on
your account has moved to the current messaging service; ask support whether yours has. A number
no contact holds gets a new contact. A number added this way shows **Text message** as its source
on the contact. Of the texts you send, only those sent by a workflow are matched. A text from a
short code or an alphanumeric sender is not matched.

Recognize texters is one switch for the whole organization, under **Admin → Contact Attributes →
Settings**, and it is off by default. Turning it off applies from the next text.

A text from the person that starts a new workflow conversation adds one to the contact's
**Conversations** count and moves its **Last Contact**. Later texts in that conversation, texts
your workflows send, and texts to a number with no workflow are matched without counting.

A text never waits on the match. It is sent or delivered whether or not the match finishes in
time.

A texting conversation shows under the contact's **Conversations** when it was started by a text
from one of the contact's phone numbers. That match is by number, so it does not depend on
Recognize texters. Texting conversations recorded before Recognize texters was released carry no
number and are not listed.

Texting in the US needs an approved 10DLC registration (**Admin → 10DLC Registration**, where
your organization has it). Until yours is approved, there are no texts to match.

## One number, one person

A phone number, email address, chat visitor id, or external id belongs to **exactly one
contact** in your organization. That rule is what lets a conversation resolve to a single
person, and it has a consequence worth knowing before you go looking for a bug.

**Adding an identifier another contact already holds does not move it.** On a contact that
already exists the save succeeds, but the value is parked on the contact you were editing
rather than attached to it: the row is badged **Belongs to another contact** and reads
`already on <contact id>` beneath. Both records then carry a **Possible duplicate** badge. To
genuinely move a number, remove it from the contact that holds it and save, then add it to the
other one. That second save attaches the value and clears the parked row. When the two records
are the same person, [merging](#merging-two-contacts) them moves every identifier at once.

**Add Contact** parks nothing. Creating a contact with a number or address another contact
already holds is refused outright, the dialog reports the refusal, and no record is created.
So free the value on the contact that holds it first, or create the contact without it and add
the identifier afterwards.

That is why a shared line (a household phone, a reception desk) shows up as one contact until
you create the second contact **without** the number, remove the number from the first, and
then add it to the second.

You can add three kinds of identifier by hand:

- **Phone** — rewritten to its international form when you leave the field, which is the form
  stored and compared. A number Tandem cannot dial is refused before you can add it.
- **Email** — case-insensitive.
- **External ID** — an id from another system, written `system:id` (for example
  `crm:12345`). Both halves are required. A system starting `zendesk.` is reserved for a link
  to a person in Zendesk, written `zendesk.<subdomain>:<user id>` with the subdomain alone and
  the user id in digits (for example `zendesk.acme:381920001`). Any other value under that
  system is refused. **Linked Records** writes these for you; see
  [Linking a contact to Zendesk](#linking-a-contact-to-zendesk).

A **chat visitor id** cannot be typed. It is minted by the chat widget in the visitor's
browser and means nothing outside it, so the widget is the only thing that ever sets one.

One save adds up to 20 identifiers and removes up to 20. A removal is staged the same way an
addition is: the row stays visible, struck through, with an undo button until you save.

## Linking a contact to Zendesk

When your organization has a [Zendesk integration](/docs/workflows/integrations/zendesk), a
contact's sidebar has a **Linked Records** section. A link points the contact at the same person
in Zendesk, and **Open in Zendesk** takes you straight to their profile there, in a new tab.

To add a link:

1. Open the person's profile in Zendesk and copy the link from the address bar, which looks like
   `https://<subdomain>.zendesk.com/agent/users/<user id>`. The user id on its own works too.
2. In the contact's **Linked Records**, paste it into **Zendesk user ID or profile link** and
   click **Add Link**.
3. Click **Save**. The link saves with the rest of the sidebar's changes.

With more than one Zendesk integration, a pasted profile link picks its own account. A bare user
id needs one chosen under **Zendesk account** first. A link to a Zendesk account your organization
has no integration for is refused with `That link is for <subdomain>.zendesk.com, which isn't
one of your Zendesk integrations.`

With exactly one link, **Open in Zendesk** also sits in the sidebar's title bar. Opening a profile
needs you to be signed in to Zendesk.

A link is stored as an [external id](#one-number-one-person), so it follows the same rules as any
identifier. A Zendesk user belongs to one contact: linking one another contact already holds parks
the link under **Identifiers** as **Belongs to another contact** and flags both contacts as
possible duplicates. Merging two contacts keeps both of their links.

A link keeps working if the Zendesk integration it was added through is removed. Its row then reads
**Not connected**. With no Zendesk integration left, new links can't be added until one is
connected again.

Deleting a contact removes its links and changes nothing in Zendesk. The person's Zendesk record
stays until you delete it there.

## Your Zendesk users

When your organization has a [Zendesk integration](/docs/workflows/integrations/zendesk), the
**Contacts** page can list the end-users in your Zendesk account as well as your contacts.
Choose **Contacts**, **Zendesk**, or **All** beside the search box. **All** shows your contacts
first and your Zendesk users below them, each with its own **Previous** and **Next**, and the
search box searches both. With more than one Zendesk integration, a picker beside the switch
chooses which account to list. It starts on the first by name.

The integration needs `users:read` (or `read`) under **Scopes**. Without it the list shows `This
Zendesk integration can't read users. Add users:read to its scopes and save it.` See [Connect it
in Tandem](/docs/workflows/integrations/zendesk#3-connect-it-in-tandem).

The Zendesk list is read-only:

- It shows end-users only. Your Zendesk agents and admins never appear, in the list or in a search.
- It shows each user's name, email, phone, and when they were last updated, and pages with
  **Previous** and **Next**.
- Clicking a row opens the user in a sidebar: their profile, notes, activity, and any custom
  fields that have a value. **Open in Zendesk** opens their profile there.
- In the **Zendesk** view, **Add Contact** and **Import CSV** are hidden. Nothing in the Zendesk
  list or its sidebar can be edited.

Every page, search, and user you open is read from Zendesk at that moment, and Tandem keeps none
of it. Each one is a request against your Zendesk account's API rate limit, which your other
Zendesk apps share. When Zendesk is rate-limiting the account, the list says so and offers
**Retry**.

The search box takes a name, an email address, a phone number, or words from a user's notes.
Zendesk can take a few minutes to make a new or changed user searchable, so a user who appears in
the list may not turn up in a search yet. A search returns at most 10,000 users.

Everyone in your organization who can open **Contacts** can see your Zendesk end-users here,
including their notes and custom fields. The list doesn't show which Zendesk users are linked to
a contact.

## Attributes

Attributes are the fields every contact can hold beyond a name and the ways to reach them.
They are defined once for the whole organization under **Admin → Contact Attributes**, and
every contact in the organization then shows one field per definition.

Each definition has:

- **Key** — lowercase letters, numbers, and underscores, starting with a letter, up to 40
  characters. This is the name agents and the API use.
- **Label** — what the field is called on a contact, up to 80 characters.
- **Kind** — what sort of value it holds, which decides the control you get and what the
  service accepts. See the table below.
- **Required** — a marker only. The field is shown as required in the contact sidebar, and
  nothing blocks a save when it is empty.
- **Agent can edit** — marks the attribute as one an agent may fill in.

<Callout type="info">
    **Agent can edit** is what decides which attributes an agent may fill in during a conversation,
    and it governs writing only. An agent that recognizes the caller is told the attribute values on
    their record whether or not this is set; an agent whose **Contacts** card does not allow updates
    can write none of them. See [Contacts on an agent](/docs/agents/voice-agents/contacts).
</Callout>

### The kinds

| Kind                  | What the service accepts                               |
| --------------------- | ------------------------------------------------------ |
| **Full name**         | Any text                                               |
| **Email**             | An email address, stored lowercase                     |
| **Phone**             | A number Tandem can dial, stored in international form |
| **Postal address**    | Any text, on a multi-line field                        |
| **Date of birth**     | A date, `YYYY-MM-DD`                                   |
| **Date**              | A date, `YYYY-MM-DD`                                   |
| **Postal code**       | Any text                                               |
| **Alphanumeric code** | Any text                                               |
| **Number**            | A number                                               |
| **Yes / no**          | Yes, No, or no value                                   |
| **Free text**         | Any text, on a multi-line field                        |
| **Choice**            | One of the options you list on the definition          |

Seven of these are checked for shape: **Email**, **Phone**, the two date kinds, **Number**,
**Yes / no**, and **Choice**. The other five — **Full name**, **Postal address**, **Postal
code**, **Alphanumeric code**, and **Free text** — accept any non-empty text up to 500
characters, so **Postal code** and **Alphanumeric code** give you a labeled field rather than a
validated one.

### Saving the list

One **Save** replaces the whole list, so every row goes up together and one invalid row holds
back every edit on the page. If somebody else saves a different list while yours is open, a
warning appears above the editor: saving replaces theirs with what is on your screen, and
reloading the page starts you from the current list instead.

Two limits and one rule to plan around:

- **50 attributes** per organization, each **Choice** kind carrying up to **50 options**.
- **Text values are capped at 500 characters** on a contact.
- **A key cannot change after it is saved.** The field locks itself once a definition has
  been stored, because every value on every contact is filed under that key: renaming it
  would orphan all of them at once. Give a definition the wrong key and the way out is to
  remove it and add another.

Removing a definition takes the field off the contact form and leaves the values alone.
They stay on each contact, read-only, under **No longer defined**, so nothing is lost if you
remove a definition by mistake. Each row there is identified by the attribute's **key**, since
the label went with the definition: remove "Loyalty Tier" and the row reads `loyalty_tier`. Add
the key back and the values are editable again.

## Importing contacts

**Import CSV**, beside **Add Contact**, creates and updates contacts from a spreadsheet. It takes
a CSV file and walks you through three steps: choose the file, match its columns to contact
fields, then follow the import while it runs.

### The file

- **CSV in UTF-8**, with a header row naming each column. In Excel, save as **CSV UTF-8**. Excel's
  other CSV formats are not UTF-8, and a file saved in one is refused if it holds an accented
  letter. Spreadsheet files such as `.xlsx` are not accepted.
- **Comma or semicolon** between fields. Tandem reads whichever the header row uses more.
- **Up to 20 MB and 50,000 rows.** A larger file is refused before it uploads. A file with more
  than 50,000 rows stops after the 50,000th; see [When an import stops](#when-an-import-stops).
- **Up to 500 columns, each name up to 200 characters.** A file past either limit is refused
  before it uploads.
- **No two columns can share a name.** A column whose header cell is blank is ignored, so the
  empty columns Excel sometimes adds after the last one do no harm. A file whose first line is
  blank, or whose header names no columns at all, is refused.

Blank rows, and rows whose cells are all empty, are skipped. They still count when rows are
numbered, so a row number in the report matches the row in your spreadsheet, but they do not
count toward the 50,000.

Excel can turn a long number into scientific notation (`1.55555E+10`), so format phone columns
as text before you export. A cell saved that way is reported as an invalid phone number.

### Matching columns

The second step lists each column with a sample value from the file. Choose the contact field
it fills: **Phone**, **Email**, **First name**, **Last name**, **Display name**, **Tags**, one
of your organization's [attributes](#attributes), or **Ignore**. Columns with common names,
such as Phone, Mobile, Email, First name, Last name, Name, and Tags, are matched for you,
ignoring case, spaces, and punctuation.

**Start Import** stays unavailable until at least one column is matched to **Phone** or
**Email**, because those are what a row is matched to a contact by. Several columns can be
**Phone** or **Email** (a mobile and an office number, say); every other field can be chosen
for one column only.

How a cell is read:

- **Phone**: spaces, dashes, and parentheses are ignored. A number without a `+` is read as North
  American when it has 10 digits, and as starting with its country code otherwise.
- **Tags**: a cell can hold several tags separated by `;` or `,`.
- **Number** attributes: digits with a period as the decimal mark, such as `3.5`. A comma as the
  decimal mark (`3,5`) or as a thousands separator (`1,000`) is not a number.
- **Yes / no** attributes: `yes`, `y`, `true`, or `1`, and `no`, `n`, `false`, or `0`, in any
  case.
- **Other attributes**: the same values a contact accepts; see [The kinds](#the-kinds).

### What each row does

Each row is matched to a contact by its phone numbers and email addresses.

- **No contact matches**: a new contact is created with the row's values.
- **One contact matches**: the import fills in only what the contact is missing. It sets a name
  or an attribute value only where the contact has none, adds tags the contact does not have (up
  to 50), and adds the row's other phone numbers and email addresses. It never changes a value a
  contact already has and never removes anything. Notes are not imported.
- **Two contacts match** (the phone number belongs to one contact and the email address to
  another): the contact holding the value in the leftmost of those columns is updated. The other
  value is parked on it as **Belongs to another contact**, and both contacts get the **Possible
  duplicate** badge. These rows are counted under **Possible duplicates**;
  [merge](#merging-two-contacts) the pair if they are one person.

Rows are imported in file order, so a later row that repeats an earlier row's phone number or
email address updates the contact the earlier row created.

A cell that can't be used is skipped and reported, and the rest of the row is still imported:
a phone number that can't be dialed, an email address that isn't valid, an attribute value of
the wrong kind, a tag longer than 40 characters, or a name longer than 120. A row left with no
usable phone number or email address is not imported and counts as **Failed**, as does a row
whose contact could not be saved.

### Following an import

The import runs on Tandem's side, and the dialog counts the rows imported so far. Closing the
dialog doesn't stop it; open **Import CSV** again in the same browser tab to see its progress.
After a page reload the dialog no longer shows it, though the import still finishes and its
contacts appear in the list.

When it finishes, the dialog shows how many rows were **Created**, **Updated**, **Possible
duplicates**, and **Failed**, and lists each row that had a problem by its row number and
column. Row numbers are spreadsheet rows: the header is row 1, so the first contact is row 2.
The list holds the first 500 problems, and never shows the value in the cell.

### When an import stops

An import is not all or nothing. If it stops part way, the rows imported before it stopped are
kept, and the dialog shows the reason:

- `file has more than 50,000 rows`: the rows after the 50,000th were not imported. Import them
  as a separate file.
- `ran out of time`: an import runs for at most 15 minutes, and a large file can need longer.
- `stopped responding`: the import recorded no progress for 2 minutes.
- `the file is not valid CSV (CsvError)`: a problem further into the file than the preview
  reads, such as a quote that is opened and never closed.
- `the import stopped on an unexpected error`, followed by the error's name in parentheses:
  import the file again, and contact support if it recurs.

Importing the same file again is safe. Rows that were already imported match their contacts and
only fill in what is still missing, so the second run picks up the rows the first never reached.

Nothing stops a second import from starting while one runs, from another browser tab for
example. Because an import only fills in what is missing, two overlapping imports of the same
file add nothing twice.

### Who can import, and what happens to the file

**Import CSV** is hidden unless you hold the administrator role, the same check deleting and
merging use, and the API checks the same role. Every role Tandem issues today carries it.

The uploaded file is deleted when the import finishes successfully. The file of an import that
stopped, or whose delete failed, is removed by storage automatically about 7 days after it was
uploaded.

## Possible duplicates

A contact carries a **Possible duplicate** badge, on its list row and at the top of its
sidebar, when an identifier somebody tried to add to it already belonged to another contact.
The pending row on the contact names the id of the record that holds the value.

Open a badged contact and its sidebar has a **Possible duplicates** section listing each contact
it was flagged against, with that contact's identifiers and the value the two collided on.
Opening it also tidies the pending rows: one whose named holder has since given the value up, or
been deleted, is dropped.

Two records that share a number may be one person with two entries or two people on one line.
Nothing in Tandem decides which; you do. If they are the same person, merge them. If they are
not, remove the identifier from the record it does not belong to and add it to the one you are
keeping; that save clears the pending row. Resolved by hand, **the badge does not clear itself**,
so treat it as "these two were once in conflict" rather than as a live warning.

### Merging two contacts

In the **Possible duplicates** section, click **Merge into This Contact** beside the other
record. The contact you have open is the one kept, and the other is merged into it. The
confirmation names both. Save or discard any edits in the sidebar first: the button is
unavailable while the contact has unsaved changes. Merging:

- **Moves every identifier** from the other record to the one kept: phone numbers, email
  addresses, chat visitor ids, and external ids. A call from any of those numbers reaches the
  kept contact from then on.
- **Combines notes and tags.** The other record's notes are added below the kept contact's. If
  the two together run past 4,000 characters, the end is cut and the message confirming the merge
  says so. Tags are combined up to the 50-tag limit.
- **Fills in attribute values** the kept contact has none for. Where both have a value, the kept
  contact's stays.
- **Brings the history along.** The kept contact's **Conversations** list includes the other
  record's conversations, and on the kept contact's next call a voice agent that recognizes callers
  reads the caller memory filed under the other record too.
- **Removes the other record** from your contacts list. In Call Logs, a call that named it now
  names the kept contact, and clicking the name opens the kept contact.

**Merging can't be undone.** A merged contact can't be split back into two.

The button is hidden unless you hold the administrator role, the same check deleting uses, and
every role Tandem issues today carries it. Merging clears the flag between the two records. A
third contact that was flagged against the merged-away record keeps its badge, and its **Possible
duplicates** section no longer lists that record.

## Deleting a contact

Delete a contact from the trash icon on its row in the list, and confirm. The icon is hidden
unless you hold the administrator role, and the API route checks the same role. Every role
Tandem issues today (Owner, Admin, and Member) carries it, so in practice everyone in your
organization can delete a contact. See [Admin](/docs/overview/admin) for what that gate does
and does not keep out.

The confirmation has a checkbox, **Also delete this person's conversation history (call
transcripts and chat transcripts)**, checked by default. Clear it to remove a duplicate or test
contact while keeping the record of what was said.

Deleting is not undoable. Whichever you choose, it removes:

- **The contact record**, including its name, tags, notes, and attribute values.
- **Its identifiers**, which frees each number, address, and id for another contact to claim.
- **Every contact merged into it.** What is left of each merged-away record goes too.
- **What your voice agents remember about this person**: the caller memory, both the
  organization-wide record and any per-agent one, for the contact, for every contact merged into
  it, for every phone number it held, and for every website chat visitor id it held.
- **Its history in reporting**: every earlier change to the contact, with the names, numbers,
  and addresses those changes carried. This goes when reporting processes the deletion, shortly
  after the contact is removed.

With the box checked, it also removes:

- **Call records and transcripts**: for every phone number it held, the disconnect-resume
  record, the call records, and the stored transcripts.
- **Website chat conversations and their transcripts**, for every chat visitor id it held.

The memory, and with the box checked the call records, transcripts, and chat conversations, are
erased **before** the contact is removed. If that fails, the delete fails with it and the contact
is still there, so you can retry rather than being left with a deleted record and its
conversation data still in place. A retry is safe and picks up where the last one stopped.

A contact deleted while it is being merged into another can have its details kept on the contact
it was merging into.

**Reporting keeps its record of every call, text, and chat**, along with the phone number it came
in on. What reporting drops is the name and the link: the conversations that pointed at the
contact no longer point at anything, and the numbers and addresses it held are removed there as
well. What stays behind is a marker under the deleted contact's id, holding nothing about the
person: no name, no numbers, no addresses. Reporting keeps it so that reprocessing an older event
cannot rebuild the contact you deleted.

Neither choice erases what a workflow recorded about a conversation as it ran: its entry in
reporting and the data it logged, which can include the text of messages.

Whichever you choose, deleting a contact does not promise to erase the text messages exchanged
with its numbers; some or all of them can remain.

Deleting a contact changes nothing in Zendesk. A person linked under **Linked Records** keeps
their Zendesk record until you delete it there.

## API

Contacts have a public API at `/v1/contacts`: list and search, create, read, update, delete,
merge, read a contact's possible duplicates, read or replace the organization's attribute
definitions, read or change the organization's contact settings, and import a CSV file. A
delete erases the conversation history unless you send `history=keep`. See the
[Contacts API](/docs/api/contacts/contacts/listContacts) reference,
[Merge contacts](/docs/api/contacts/contacts/mergeContact) for the merge, and
[Start an import](/docs/api/contacts/contacts/startImport) for the import. An import through
the API takes the same three calls the dialog makes: ask for an upload URL, `PUT` the file to
it, then start the import and read its progress until it finishes.
