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

# GoHighLevel Integration

> Sync contacts, map owners, and use GHL data in campaigns. Native integration for Tuco AI iMessage automation — works alongside HubSpot, Salesforce.

## What You Get

* **Contact sync** — Import GHL contacts into Tuco with deduplication
* **Owner mapping** — Map GHL users to Tuco team members
* **Tag sync** — GHL contact tags imported automatically
* **Custom field sync** — Map GHL custom fields to Tuco custom fields or tags
* **Campaign conditions** — Use GHL tags in campaign branching and stop conditions

## Connecting GoHighLevel

1. Go to **Integrations → CRM**
2. Enter your GHL **API key** and **Location ID**
3. Click **Test & Connect**
4. A full sync starts automatically

<Note>Only one CRM can be connected at a time. Disconnect HubSpot before connecting GHL.</Note>

### Required API scopes

* Location access
* Contacts: search and read
* Tags: read location tags

### Recommended scopes

* Users: read (for owner mapping)
* Custom fields: read (for field mapping)
* Contact tags: add/remove (for campaign automation)
* Conversations: create (for activity logging)

## Contact Sync

### How it works

* **First sync** — imports all GHL contacts into a "GHL Import" list
* **Incremental sync** — every 15 minutes, syncs recently modified contacts
* **Deduplication** — matched by phone number
* **Tags** — GHL contact tags are imported into Tuco's tag system

### What syncs

| GHL Field   | Tuco Field          |
| ----------- | ------------------- |
| firstName   | First Name          |
| lastName    | Last Name           |
| email       | Email               |
| phone       | Phone               |
| companyName | Company             |
| tags        | Tags                |
| assignedTo  | Owner (via mapping) |

## Owner Mapping

Same as HubSpot — map GHL users to Tuco members.

1. **Integrations → CRM → Owner Mapping**
2. Map each GHL user to a Tuco member
3. **Save & Re-sync**

## Custom Field Mapping

Map GHL custom fields to Tuco tags or custom fields.

1. **Integrations → CRM → Property Mapping**
2. Select a GHL custom field
3. Choose Tag or Custom Field target
4. **Save & Re-sync**

## Channel control with tags

Use a GHL **contact tag** to control which channel a lead's outbound messages go out on. Tags are read live on every send (workflow action **and** conversation-provider / CloseBot), so they take effect immediately — no sync, no custom field, no reinstall.

| Tag on the contact | Effect                                                                                                                                                                                                    |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `forcefallback`    | **Skip the iMessage check** and send via the workspace's configured fallback (SMS — Twilio / GHL / custom webhook). Use it for leads you already know aren't on iMessage, or that you simply want on SMS. |
| *(no tag)*         | **Normal flow** — try iMessage first, fall back only if the contact isn't reachable on iMessage.                                                                                                          |

The tag name is matched loosely (case-insensitive, ignoring spaces/dashes/underscores), so `forcefallback`, `force-fallback`, `force_fallback`, and `Force Fallback` all work.

<Note>
  `forcefallback` only changes the **channel**, not delivery. The lead must have a fallback
  configured (**Settings → When iMessage isn't available**). With no fallback set up the
  send returns an error instead of silently delivering over iMessage. Add or remove the tag
  any time to flip a lead between iMessage-first and SMS-only.
</Note>

## Send iMessages from a GHL workflow

The simplest way to trigger a Tuco iMessage from a GHL workflow is a plain **Webhook**
action pointed at the Send Message API — no Marketplace app or action secret, just your
Tuco API key. GHL's Webhook action already attaches the full contact payload, so **the
only thing you map is the message**.

1. In your workflow, add a **Webhook** action — Method `POST`, URL `https://app.tuco.ai/api/messages`.
2. Add a header: `Authorization` = `Bearer tuco_sk_xxxxxxxxxxxxx` (your [API key](/api-reference/api-keys)).
3. Under **Custom Data**, set `message` (required). Optionally add `lineId` (pin a sending line) and `attachmentUrls`.

Tuco reads GHL's native contact fields to fill in the recipient and contact link
automatically (`phone` → recipient, `contact_id` → GHL contact, `location.id` → GHL
location, `full_name` → name, `user.email` → owner). You don't map any of those.

<Note>
  Full field-by-field mapping and a copy-paste example are on the
  [Send Message → "Send straight from a GoHighLevel webhook"](/api-reference/endpoint/send-message#send-straight-from-a-gohighlevel-webhook)
  reference. If the contact has no phone or email the send fails with `missing_recipient`.
</Note>

<Tip>
  Want Tuco to look up the recipient, pick a line, and run compliance checks for you from
  only a contact ID? Use the GHL Marketplace **Send via Tuco** action instead — see your
  in-app **Leads → AI & API** tab for both setups.
</Tip>

## Send a voice note from GHL

The Marketplace **Send via Tuco** (Send iMessage) action can deliver a native iMessage
**voice note** — the recognizable play-button bubble, not a file link. You supply a clean
audio URL and flip one toggle; Tuco transcodes the audio to the voice-note format for you.

### Get an audio URL

You need a public `https://…` link to an audio file (`mp3`, `m4a`, `wav`, `aac`, `ogg`,
or `caf`), **≤ 25 MB** and **≤ 5 minutes**. The easiest way to get one:

* In the dashboard, open **/uploads** (or the **Upload audio etc** tab on the **Leads**
  page), drag in your clip, and hit **Copy** to copy the link.
* Or upload it programmatically with
  [`POST /api/attachments/upload`](/api-reference/upload-attachment).

Either way the URL belongs to your workspace, so it passes Tuco's attachment
ownership guard automatically.

### Configure the action fields

In your GHL workflow, add the **Send via Tuco** action and set these fields:

| Field           | Required                   | Value                                                                                                                                                                                                         |
| --------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `contactId`     | **Yes**                    | The contact to send to (map from the trigger contact).                                                                                                                                                        |
| `message`       | **Yes**                    | Set the **Default value** to `_attachment_only_` — a magic word that sends the audio with **no caption**. GHL requires this field to be non-empty; leaving it truly blank errors with `Missing message`.      |
| `attachmentUrl` | **Yes**                    | A clean `https://…` audio URL (see above). Must be `mp3`/`m4a`/`wav`/`aac`/`ogg`/`caf`, ≤ 25 MB, ≤ 5 min.                                                                                                     |
| `voiceNote`     | **Yes (for a voice note)** | Set the **Default value** to `true`. Without it the audio sends as a **plain file**, not a native voice memo.                                                                                                 |
| `skipIfReplied` | Optional                   | Leave blank, or `true`, or a window like `7d` / `1mo` to skip the send when the contact has already replied. See [`skipIfReplied`](/api-reference/endpoint/send-message#skip-if-the-contact-already-replied). |

<Warning>
  A voice note — like any attachment — is **blocked on the first-ever message** to a
  contact. It only lands on a contact you already have a conversation with. Warm the
  contact up with a plain text message first; the voice note lands on any later step.
</Warning>

<Note>
  `_attachment_only_` needs a real `attachmentUrl` to go with it. If you set the magic
  word but leave the URL empty, the action returns
  `Used _attachment_only_ but no Attachment URL was provided`. To send audio **with** a
  caption, put your caption in `message` instead of the magic word.
</Note>

<Tip>
  Want the audio to send as a **file** rather than a voice note? Leave `voiceNote` off
  (or `false`). Everything else stays the same.
</Tip>

## Check for Replies (Marketplace workflow action)

The **Check for Replies** action tells your workflow whether a contact has replied on
iMessage since your last outbound message — and hands the recent conversation to later
steps, so If/Else branches, AI steps, and webhooks can act on what was actually said.

The action takes an optional `phone` field; leave it blank to use the contact's phone
automatically.

### Response

```json theme={null}
{
  "hasReplied": true,
  "hasEverReplied": true,
  "lastReply": "yes, tomorrow at 2 works!",
  "pastConversation": [
    {
      "from": "+13022373592",
      "to": "+19042956129",
      "text": "hi — still interested?",
      "attachmentUrls": [],
      "direction": "sent",
      "fromLineId": "664f1c7fec719ac5207d6e9c",
      "status": "delivered",
      "at": "2026-07-14T10:00:00.000Z"
    },
    {
      "from": "+19042956129",
      "to": "+13022373592",
      "text": "yes, tomorrow at 2 works!",
      "attachmentUrls": [],
      "direction": "received",
      "fromLineId": "664f1c7fec719ac5207d6e9c",
      "status": "delivered",
      "at": "2026-07-14T10:05:00.000Z"
    }
  ]
}
```

| Field              | Type    | Description                                                                                                                                                              |
| ------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `hasReplied`       | boolean | `true` when the contact's newest inbound message is newer than your last outbound to them (or when they've messaged you and you never sent anything).                    |
| `hasEverReplied`   | boolean | `true` when the contact has ever replied, no matter what you've sent since. Stays `true` after follow-ups and auto-replies that would flip `hasReplied` back to `false`. |
| `lastReply`        | string  | Full text of the contact's most recent reply. Empty string when they've never replied.                                                                                   |
| `pastConversation` | array   | The last 10 messages exchanged with the contact, both directions, oldest → newest. Empty when there's no conversation on record.                                         |

Each `pastConversation` item:

| Field            | Type      | Description                                                                                                                       |
| ---------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `from` / `to`    | string    | Sender and recipient. For `direction: "sent"` that's your Tuco line → the contact; for `"received"` it's the contact → your line. |
| `text`           | string    | Message body (empty for attachment-only messages).                                                                                |
| `attachmentUrls` | string\[] | URLs of any attachments on the message.                                                                                           |
| `direction`      | string    | `"sent"` (you → contact) or `"received"` (contact → you).                                                                         |
| `fromLineId`     | string    | The Tuco line the exchange happened on.                                                                                           |
| `status`         | string    | Message status (`sent`, `delivered`, `failed`, …) — failed sends are included, so check `status` if delivery matters.             |
| `at`             | string    | ISO 8601 timestamp.                                                                                                               |

<Note>
  `hasReplied` and `hasEverReplied` answer different questions. `hasReplied` means
  "replied *since your last outbound*" — after you (or Tuco's AI copilot) send a
  follow-up, it flips back to `false`. `hasEverReplied` never flips back: once the
  contact has replied, it stays `true`. If your workspace has an auto-responder,
  branch on `hasEverReplied` — the auto-reply usually lands within seconds, so
  `hasReplied` will already be `false` by the time your workflow checks. Use
  `hasReplied` only for strict wait-for-reply flows, and `lastReply` /
  `pastConversation` for AI context.
  If a line was later deleted, `from`/`to` may be empty for its messages — `fromLineId`
  still identifies the line.
</Note>

## Using GHL Data in Campaigns

### Stop conditions

* **GHL tag** — stop if contact has/doesn't have a tag (live API call)

### Branching

* **GHL has tag** — branch based on GHL tags (live API call)
* **GHL property** — branch on custom field values

All GHL checks call the **live GHL API**, not cached data.

### Available operators

When comparing GHL property values in stop conditions or branches, all standard operators are supported:

| Operator                | Label                   | Use case                   |
| ----------------------- | ----------------------- | -------------------------- |
| `eq`                    | equals                  | Exact match                |
| `neq`                   | not equals              | Exclude a value            |
| `contains`              | contains                | Partial text match         |
| `not_contains`          | does not contain        | Exclude partial match      |
| `starts_with`           | starts with             | Prefix match               |
| `gt` / `gte`            | greater than (or equal) | Numeric comparison         |
| `lt` / `lte`            | less than (or equal)    | Numeric comparison         |
| `gt_date` / `lt_date`   | after / before date     | Date comparison            |
| `exists` / `not_exists` | exists / is missing     | Check if field has a value |

### Goal Bot integration

GHL tags and custom fields can be used as advance or exit conditions in [Goal Bots](/features/goal-bots). When a Goal Bot checks GHL data, it calls the live API — the same way campaigns do.
