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

> ## Agent Instructions
> Use the public API base URL: https://api.teamfollowup.ai/api.
> Authenticate public API requests with Authorization: Bearer YOUR_API_KEY.
> Use product-owned terms: agents, campaigns, projects (GoHighLevel sub-accounts), contacts, calls, outcomes, skills, cadences, phone numbers.
> Scope requests by locationId rather than projectName.
> Read the guide linked from each API reference group before recommending an endpoint.

# Contacts without a CRM

> When Team Follow Up is the system of record: how contacts get in, what a contact holds, the custom fields your prompts read, and what happens when you delete one.

A sub-account can keep its contacts in GoHighLevel, or keep them here. When it
keeps them here there is nowhere else they exist, so this is the **system of
record**: what you write is what the agent dials and what the prompt reads.
Nothing syncs it back from somewhere else, and nothing overwrites it overnight.

Everything on this page is the [Native Contacts API](/api-reference/native-contacts).
The ordinary [Contacts API](/api-reference/contacts) still works on these
sub-accounts too, and is the one to use when you want calling history across
every sub-account at once, whatever CRM each of them uses.

<Note>
  Which one a sub-account is comes from its `crm` field, in
  `GET /api/v2/projects/{id}`. `native` means it keeps contacts here. Every
  endpoint on this page answers `400` for a sub-account with a CRM connected.
</Note>

## What a contact holds

Deliberately the minimum a call needs, plus whatever you define on top.

| Field                                     | Meaning                                                                                    |
| ----------------------------------------- | ------------------------------------------------------------------------------------------ |
| `_id`                                     | The contact id. Always starts with `nct_`.                                                 |
| `phone`                                   | Required, and unique in the sub-account. Stored as E.164.                                  |
| `firstName`, `lastName`, `email`          | Who they are.                                                                              |
| `timezone`                                | An IANA zone such as `America/Chicago`. Empty means the campaign's calling window decides. |
| `address1`, `city`, `state`, `postalCode` | The postal address, in the shape the in-call address tool writes.                          |
| `customFields`                            | Your own fields, keyed by field key.                                                       |
| `caller_pref`                             | The contact's memory: what earlier calls learned, and what people added by hand.           |
| `doNotCall`                               | Whether anything may dial them, with when it was decided, why, and through which door.     |
| `callCount`, `lastOutcome`                | What has happened so far.                                                                  |

## Getting contacts in

Four doors, one import. They all end up at the same place, so a number that
arrives twice updates one contact rather than making two.

<CardGroup cols={2}>
  <Card title="One at a time, from code" icon="code">
    `POST /api/contacts/add/{locationId}` with an API key. See
    [Add a contact](/api-reference/contacts/add-a-contact).
  </Card>

  <Card title="From a form, Zapier or Make" icon="link">
    The sub-account's lead link, below. No header to set.
  </Card>

  <Card title="A file" icon="upload">
    [Import contacts from CSV](/api-reference/native-contacts/import-contacts-from-csv),
    or the same button on the Contacts page.
  </Card>

  <Card title="By hand" icon="user-plus">
    [Create a native contact](/api-reference/native-contacts/create-a-native-contact),
    or **Add contact** on the Contacts page.
  </Card>
</CardGroup>

### Phone numbers are normalised, not guessed

A ten-digit number is read as North American. Anything else must carry `+` or
`00`. A number that cannot be made into E.164 is **refused**, with the reason,
rather than being stored as somebody else's number:

```json theme={"dark"}
{ "success": false, "error": "\"07700 900123\" is not a phone number we can dial. Include the country code, for example +44." }
```

That is the one validation worth designing your import around. In a CSV it
costs you a named line in `skipped`; from the API it costs you the request.

### The lead link

`GET /api/native-contacts/lead-webhook` returns a token. Post the same body you
would send to **Add a contact** to:

```
https://api.teamfollowup.ai/api/native-leads/{locationId}/{token}
```

It reads the lead exactly as the other doors do, triggers included.

<Warning>
  The token in that link is its only credential. Anyone holding it can add
  contacts to the sub-account. Keep it out of client-side code and anywhere a
  log would carry it, send it over HTTPS only, and use an API key instead
  wherever your sender can set a header. If it leaks,
  [rotate it](/api-reference/native-contacts/rotate-the-lead-link): the old
  link stops working at once, with no grace period, so change your form or Zap
  in the same sitting.
</Warning>

## Finding them again

[List native contacts](/api-reference/native-contacts/list-native-contacts)
pages newest first. `search` matches a first name, a last name, an email or any
part of the number, so a caller id in any spelling finds the row.

`outcome` narrows to one of two: `transferred` for the people a call handed to
somebody, who are still callable, and `not_qualified` for the people who asked
not to be called, by either door. `total` counts the whole filtered set rather
than the page you were given.

## Custom fields

A sub-account defines its own fields, and their values live on each contact.

The **key** is derived from the label once and never changes, so renaming
"Budget" to "Monthly budget" cannot break a prompt that reads `{{budget}}`.
Types are `text`, `number`, `date` and `select`, and a value is checked against
the type: a `date` must be `YYYY-MM-DD`, a `select` must be one of its options,
case-insensitively.

```bash theme={"dark"}
curl -X POST "https://api.teamfollowup.ai/api/native-contacts/fields?locationId={locationId}" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label":"Service needed","type":"select","options":["Implants","Whitening","Check-up"]}'
```

Set values with `customFields` on a create or an update. An empty string clears
one, which is also the only way to let a "field is set" trigger fire again.

<Note>
  A label whose key would collide with a standard call variable, such as
  `name`, `phone`, `timezone` or `caller_pref`, is refused. Custom fields are
  applied to a call's variables after the engine's own, so such a field would
  silently replace the lead's real name, or their memory, in the prompt.
</Note>

## Do not call

`POST /api/native-contacts/{contactId}/do-not-call` with `doNotCall: true`
stops every campaign, cadence and test dialling that contact. Send `false` to
clear it. The `reason` you send is kept, and an opt-out the contact made on a
call keeps its own words rather than taking yours.

**The decision outlives the contact.** Delete a suppressed contact and the
number keeps its marker. If that number is added again later, the new contact
comes back already suppressed, carrying the original date, reason and source,
and the response says so:

```json theme={"dark"}
{
  "success": true,
  "data": { "_id": "nct_...", "doNotCall": true, "suppressedFromHistory": true }
}
```

You did not ask for that, which is exactly why you are told. A lead you have
paid for and that nothing will ever dial must not come back as a bare success.
If you believe the suppression is wrong, clear it deliberately with the
do-not-call endpoint.

## Deleting a contact

Deleting is not just removing a row.

<Steps>
  <Step title="Ask what it would take with it">
    [Preview the delete](/api-reference/native-contacts/preview-a-native-contact-delete).
    It names the live bookings, says whether the contact is on a call right
    now, and returns the appointment ids.
  </Step>

  <Step title="Pass those ids back">
    Send them as `expectedAppointmentIds`. If the contact has been booked again
    since you looked, the delete is refused with `409` rather than quietly
    cancelling something nobody saw.
  </Step>

  <Step title="Bookings are cancelled first">
    A delete that cannot cancel one of them is refused whole, and the error
    names which. Removing the row without cancelling would leave the
    appointment live, the seat held and the attendee expecting it.
  </Step>
</Steps>

Deleting several at once is
[bulk delete](/api-reference/native-contacts/delete-several-native-contacts).
It is partial on purpose and answers with `deleted`, `failed` and `remaining`:
one contact who is on a call must not keep the other forty-nine, and the run
stops at a time budget rather than running past the point where you stopped
waiting. Send `remaining` again to carry on.

<Warning>
  A delete is not erasure. The contact's do-not-call marker stays, because that
  is the only thing stopping a later import bringing the number back clear.
</Warning>

## Starting a campaign for contacts you already have

A new contact starts any campaign whose trigger is "New contact". For contacts
that were already here when the campaign was made, use
[Put contacts into a campaign](/api-reference/native-contacts/put-contacts-into-a-campaign).

It answers `202` with what was accepted and a count of what was skipped, by
reason. A contact on do-not-call is always skipped, and nothing you send
overrides that.

## Contact memory

`caller_pref` is one block of text the next call reads. The post-call step
writes what the agent learned; you can add what only a person knows:

```bash theme={"dark"}
curl -X POST "https://api.teamfollowup.ai/api/native-contacts/{contactId}/memory?locationId={locationId}" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"Partner has to approve anything over £2,000."}'
```

Notes are the other half of that pair and the distinction matters: a **note**
is for people and appears on the timeline; **memory** is read by the agent on
the next call.

## Where to go next

<CardGroup cols={2}>
  <Card title="Native Contacts API" icon="code" href="/api-reference/native-contacts">
    Every operation, with schemas and examples.
  </Card>

  <Card title="Calendars" icon="calendar" href="/calendars/overview">
    Booking without a CRM: the diary these contacts are booked into.
  </Card>
</CardGroup>
