Skip to main content
A contact is one person, keyed by phone number, that a sub-account can call: a lead that has just arrived and has never been dialled, one part-way through a cadence, one already called, or somebody who called you. The record exists from the moment the lead arrives, which is why a contact with callCount of 0 is an ordinary contact and not a missing one. The contact record is where you answer “how many times did we try, what happened, and when do we try again?” It is also where you say “never call this person”. Where contacts live depends on the sub-account. One with a CRM connected keeps its contacts there, and the platform keeps the calling history against each CRM contact. A sub-account with no CRM keeps its contacts on the platform: add them in the dashboard, import a CSV, post them with your API key, or point a form at the sub-account’s lead link. See Adding contacts by API, and Contacts without a CRM for managing them afterwards: custom fields, do-not-call, deletion and the lead link.

What one contact holds

GET /api/contacts returns one row per unique contact.

How old a lead was when it arrived

A lead is usually created in the CRM and sent to the platform seconds later. A lead from an old list is different: it was created months ago and only sent now. The gap between leadGeneratedAtInCrm and leadReceivedByTfuAi tells the two apart.
  • leadGeneratedAtInCrmPrecision is time when the CRM sent the exact moment, and day when it sent only the date. A day value reads as midnight UTC, so measure gaps under a day only when it is time.
  • leadReceivedByTfuAi is null for leads received before it was recorded. For those, dateAdded gives the day only.
  • leadGeneratedAtInCrm is null when the CRM sent no creation date.
GET /api/contacts/{phoneNumber}/calls returns everything the platform has done with one person: their calls and a dispositionSummary counting how the calls ended.

Outcome types on a contact

A contact’s disposition.type and the disposition filter use these values: These are the contact-level names. The per-call outcome constants that workflows route on, such as SUCCESS and KILL_SWITCH, are listed in What your agent records.

Five ways to stop calling someone

They look similar and they are not. Pick the one that matches who decided and how wide it reaches. How far each way of stopping calls reaches. Opting out on a call, Stop calling and a booking block every campaign in the sub-account; callbacks and reminders still go after a booking. An inactive tag blocks only the campaign that lists it. How far each way of stopping calls reaches. Opting out on a call, Stop calling and a booking block every campaign in the sub-account; callbacks and reminders still go after a booking. An inactive tag blocks only the campaign that lists it.
On GoHighLevel, the do-not-call flag and an opt-out are the same tag. Clearing the flag also clears an opt-out the lead asked for on a call, so check why a contact was stopped before you clear it.
Exit tags and GoHighLevel DND need the sub-account to use GoHighLevel. A sub-account that keeps its contacts here has one flag for both the opt-out and do-not-call, shown as the Do not call switch, and a booking pauses calls there too; see Contacts without a CRM.
Clearing a do-not-call flag can produce a call that should not be placed. Treat doNotCall: false as a deliberate act, and scope API keys that hold contacts:write accordingly.

Your own test leads

You will dial yourself and your team while building. Mark those contacts as internal with PATCH /api/contacts/{contactId} and isInternal: true, identifying the contact by contactId, or by phone number with idType: phone, with the project’s locationId. The response’s actionResults reports each flag as saved, or unconfirmed when the save failed part-way. In the dashboard it is Mark as internal on the contact, and it covers that phone number across the sub-account, past calls included.
  • Their calls are left out of every performance figure: lead counts, booking and transfer rates, pickup, trends and the heatmap.
  • Their cost is kept. The minutes were really spent and really billed, so the dashboard stays in step with your wallet.
  • They still get called. The mark changes reporting only.
GET /api/contacts lists them by default with isInternal set on each row. Send internal=only to see just them, or internal=exclude to hide them. Clear the mark with isInternal: false.

Adding contacts by API

For a sub-account that keeps its contacts on the platform (no CRM connected), post one lead at a time:
The key needs contacts:write. phone is required. first_name, last_name (or name), email, timezone, address1, city, state and postalCode are optional, and so is any of the sub-account’s custom fields, by its key or its name. A number already in the sub-account updates that contact instead of adding it twice, and a blank never erases a value. A new contact starts any campaign whose trigger is “New contact”. The response is 201 for a new contact and 200 for an update, with the contact’s id either way. A form, Zapier or Make step that cannot set a header can post the same body to the sub-account’s lead link instead, shown under API and webhook on the Contacts page. The secret in that link is its only credential: keep it private, and regenerate it from the same place if it leaks. A sub-account with a CRM keeps its contacts there and answers 400. Reading, updating and deleting those contacts, defining the custom fields your prompts read, and rotating the lead link are all in Contacts without a CRM.

Finding contacts

Sort with sortBy (leadName, phoneNumber, projectName, dateAdded, callCount) and sortOrder (asc, desc). Page with page and limit. Project ids come from GET /api/v2/projects; disposition takes the outcome constants in What your agent records. On one contact, include accepts calls to return their calls with the record.

Boundaries

  • A contact is unique per phone number within a project.
  • projectName on a write is a display label only. locationId is the tenant identity.
  • Marking a contact internal never suppresses a call. Use the do-not-call flag for that.

In the API