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

# Route chats based on customer data synced from Salesforce

> Build a service that looks up each customer's Salesforce Contact by email, writes their plan details onto their Text customer profile, and routes the chat to the matching team.

This guide is for teams that store customer data, such as plan tier, in [Salesforce](https://www.salesforce.com/) and want to have this data available in Text for better customer prioritization in chat routing.

By the end of this guide, you'll have a service that reads each customer's plan tier from Salesforce, saves it on their Text profile for future reporting and segmentation, highlights priority customers, and routes all incoming chats to the matching team.

## Prerequisites

* A [Text account](https://accounts.livechat.com/signup?landing_page=https%3A%2F%2Fwww.text.com%2F\&client_id=fc7bf1555adb6d3d2a441f10fd5b6eb5\&redirect_uri=https%3A%2F%2Fwww.text.com%2Fapp\&response_type=token\&source_type=website\&source_id=header-signup\&source_url=https%3A%2F%2Fwww.text.com%2F) on the Growth plan or higher, with administrator access and API access permission.
* The Text [AI Agent](https://www.text.com/features/ai-agent/) enabled and answering new chats.
* A [Salesforce](https://www.salesforce.com/) account with administrator access and API access enabled.
* [Docker with Docker Compose v2](https://docs.docker.com/get-started/get-docker/), to build and run the service in a container.
* [`cloudflared`](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/), to give the local service a temporary public HTTPS address. Skip it if the service already has one.
* [Git](https://git-scm.com/downloads) to clone the sample repository.

<GitHub.Repo repo="livechat/salesforce-route-chat-guide" variant="flat" />

This guide is based on a sample service repository. You set up Salesforce yourself, with a tier field on Contacts and an External Client App that lets the service read them. On startup, the service creates the `salesforce_plan_tier` property and the required chatting teams in Text if they don't exist.

## How a chat gets routed

1. Text sends a webhook when a new chat starts.
2. The service finds the customer's Contact in Salesforce by email. If there's no email, no single matching Contact, or an empty tier, the service skips the chat.
3. It saves the tier on the customer's Text profile.
4. For Enterprise, it also sets the customer's [relationship level](/docs/api/customer-data-platform/customers/update-customer#body-relationship-level-one-of-0) to VIP. Other tiers leave it unchanged.
5. It transfers the chat based on the customer plan tier: Basic stays with the AI Agent, Standard goes to the Regular team, and Enterprise goes to the Enterprise team.

<Warning>
  Before deploying the service, review the [production
  considerations](#prepare-the-design-for-production) for reliable webhook
  handling and routing team membership.
</Warning>

## Set up Salesforce

<Steps>
  <Step title="Prepare the Contact tier field">
    In Salesforce, find the gear icon in the upper right corner, and go to **Setup → Object Manager → Contact**.

    <img alt="Salesforce object manager" src="https://mintcdn.com/text-56a1eab5/2Nwmewcm_epVfTJP/public/images/guides/salesforce-object-manager.png?fit=max&auto=format&n=2Nwmewcm_epVfTJP&q=85&s=bfced37d487ee483a716d09010addec1" width="1559" height="513" data-path="public/images/guides/salesforce-object-manager.png" />

    With **Contact** opened, go to **Fields & Relationships** and check for an existing plan tier field. If you don't have one yet, select **New → Picklist**, label it **Plan Tier**, and enter `Basic`, `Standard`, and `Enterprise` as its values. Leave the **Default Value** empty.

    Leave the remaining field settings at their default values, then save the field.

    Once the field is saved, open its details from the **Fields & Relationships** list, and note the field's **API Name** (for example, `Plan_Tier__c`). You'll set this as `SALESFORCE_PLAN_FIELD` later.

    <img alt="Salesforce field details" src="https://mintcdn.com/text-56a1eab5/2Nwmewcm_epVfTJP/public/images/guides/salesforce-field-details.png?fit=max&auto=format&n=2Nwmewcm_epVfTJP&q=85&s=3be095e506cfdca2d6a951dc9af2bc95" width="1397" height="321" data-path="public/images/guides/salesforce-field-details.png" />

    <Note>
      The service matches these three plan tier values exactly, including capitalization. If you use different names, update the `switch` in the `internal/router/handler.go` file before you start the service. Other values are still saved to the customer profile, but they don't route the chat or set VIP.
    </Note>
  </Step>

  <Step title="Create test Contacts">
    From the **App Launcher**, open **Sales** and go to **Contacts**. Create or edit three test Contacts with distinct email addresses, and assign one tier to each Contact.

    You'll use these addresses to start test chats later. The email address that a customer provides in Text must match their Contact's email address.

    Multiple Salesforce Contacts sharing the same email cause that chat to be skipped.
  </Step>

  <Step title="Create an integration user">
    The service signs in to Salesforce as a user of its own, so it needs permission to read your Contacts. It never edits them.

    1. Create a user for this service, or pick an existing one. For testing, you can use your own user.
    2. In **Setup → Users → Permission Sets**, create a new permission set. Name it, for example, **Contacts API Read Access** and save changes.

    <img alt="Salesforce permission set" src="https://mintcdn.com/text-56a1eab5/2Nwmewcm_epVfTJP/public/images/guides/salesforce-permission-set.png?fit=max&auto=format&n=2Nwmewcm_epVfTJP&q=85&s=59e109b8498d4c2cf8564c7895dba816" width="1380" height="640" data-path="public/images/guides/salesforce-permission-set.png" />

    3. In the permission set, go to **System → System Permissions** and enable **API Enabled**. This lets the user sign in through the API, but it doesn't grant access to any data.
    4. Under **Object Settings → Contacts**, grant **Read** access to the Contact object, and read access to the **Email** and **Plan Tier** fields.
    5. Assign the permission set to that user.
  </Step>

  <Step title="Create an External Client App">
    Open **Setup** and go to **Apps → External Client Apps → External Client App Manager**. Create a new External Client App for this service.

    <img alt="Salesforce app details" src="https://mintcdn.com/text-56a1eab5/2Nwmewcm_epVfTJP/public/images/guides/salesforce-app-details.png?fit=max&auto=format&n=2Nwmewcm_epVfTJP&q=85&s=469be6f1f142d305302488576efcce45" width="1721" height="340" data-path="public/images/guides/salesforce-app-details.png" />

    In the External Client App configuration, enable OAuth and set the **Callback URL** to `http://localhost` — it's fine during testing since the app doesn't make any redirects. Add the `api` scope and check **Enable Client Credentials Flow** under **Flow Enablement**. Then **Create** the app.

    <img alt="Salesforce app OAuth settings" src="https://mintcdn.com/text-56a1eab5/2Nwmewcm_epVfTJP/public/images/guides/salesforce-app-oauth-settings.png?fit=max&auto=format&n=2Nwmewcm_epVfTJP&q=85&s=3f61cf567a9c55296fca776168f1e396" width="1442" height="858" data-path="public/images/guides/salesforce-app-oauth-settings.png" />

    On the **Policies** tab for the External Client App, open **OAuth policies**, and under **OAuth Flows and External Client App Enhancements**, enable **Client Credentials Flow**. Provide the integration user you just created as its execution user — it's the user the app signs in as. Save the changes.

    <img alt="Salesforce app policies settings" src="https://mintcdn.com/text-56a1eab5/2Nwmewcm_epVfTJP/public/images/guides/salesforce-app-policies-settings.png?fit=max&auto=format&n=2Nwmewcm_epVfTJP&q=85&s=7cd95130c333cfe0b0e0bb3b2d3e8fc0" width="1721" height="847" data-path="public/images/guides/salesforce-app-policies-settings.png" />

    Open **Settings → OAuth Settings** for the External Client App, and save its **Consumer Key** and **Consumer Secret**.

    Find your organization's **My Domain** URL under **Setup → My Domain**. A production URL looks like `https://your-company.my.salesforce.com`. A sandbox URL looks like `https://your-company--sandbox.sandbox.my.salesforce.com`. You'll set this as `SALESFORCE_LOGIN_URL`.

    <Warning>
      The client credentials flow only works through your organization's My Domain URL, not the shared `login.salesforce.com` or `test.salesforce.com` URLs.
    </Warning>
  </Step>
</Steps>

## Set up Text

<Info>
  Text calls routing destinations *teams* in the app. Its APIs and the sample
  service call them *groups*.
</Info>

<Steps>
  <Step title="Create a personal access token">
    In **Settings → API access → [Personal access tokens](https://www.text.com/app/settings/integrations/api-access/personal-access-tokens)**, create a token with these [scopes](/docs/authentication/scopes):

    * `customers:rw` — to read and write the customer's plan-tier property.
    * `chats--all:rw` — to check chat status and transfer chats in any group.
    * `groups--all:rw` — only if Regular or Enterprise doesn't exist yet and you want the service to create it. Use `groups--all:ro` instead if you create both groups manually first.

    Copy the Base64-encoded credential Text displays — the combined `account_id:personal_access_token` value.

    <Warning>
      Treat the personal access token like a password. Store it in a secrets
      manager for a deployed service and never commit it.
    </Warning>
  </Step>

  <Step title="Clone and configure the service">
    ```shell theme={null}
    git clone https://github.com/livechat/salesforce-route-chat-user.git
    cd salesforce-route-chat-user
    cp .env.example .env
    ```

    The service routes chats to two Text teams: **Regular** for Standard-tier customers and **Enterprise** for Enterprise-tier customers. If the teams don't exist, it creates them with the agents you list in `.env`.

    Fill in `.env` with the values from the previous steps:

    | Setting | Value |
    | - | - |
    | `TEXT_BASIC_AUTH` | The Base64-encoded Text PAT credential. |
    | `TEXT_REGULAR_GROUP_NAME` | Name of the Text team that receives Standard-tier chats. The service uses the team with this name or creates it if it doesn't exist. Format it exactly as it appears in Text. Defaults to `Regular`. |
    | `TEXT_ENTERPRISE_GROUP_NAME` | Name of the Text team that receives Enterprise-tier chats. The service uses the team with this name or creates it if it doesn't exist. Format it exactly as it appears in Text. Defaults to `Enterprise`. |
    | `TEXT_REGULAR_AGENT_IDS` | Comma-separated Text agent logins (email addresses) for the Regular team. Required if its API group doesn't exist. For a group that exists, the service verifies any logins you provide but doesn't add them. |
    | `TEXT_ENTERPRISE_AGENT_IDS` | Comma-separated Text agent logins (email addresses) for the Enterprise team. Required if its API group doesn't exist. For a group that exists, the service verifies any logins you provide but doesn't add them. |
    | `SALESFORCE_LOGIN_URL` | Your organization's My Domain URL, without a path. |
    | `SALESFORCE_CLIENT_ID` | The **Consumer Key** for the Salesforce External Client App. |
    | `SALESFORCE_CLIENT_SECRET` | The **Consumer Secret** for the Salesforce External Client App. |
    | `SALESFORCE_PLAN_FIELD` | The tier field's exact API Name, such as `Plan_Tier__c`. |

    Leave `TEXT_WEBHOOK_SECRET` empty for now — you'll add it after creating the webhook in a later step.
  </Step>
</Steps>

## Run the service

<Steps>
  <Step title="Get a public URL">
    Text delivers webhooks to a public internet address, so if you're running the service on your own computer rather than a server, start a [Cloudflare Quick Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/) in another terminal:

    ```shell theme={null}
    cloudflared tunnel --url http://localhost:8080
    ```

    Keep it running and copy the HTTPS address it prints, such as `https://example.trycloudflare.com`. For a deployed service, use its public HTTPS address instead.
  </Step>

  <Step title="Create the Text webhook">
    In the Text app, open **Settings → API access → [Webhooks](https://www.text.com/app/settings/integrations/api-access/webhooks)**. Create a webhook for the [`incoming_chat`](/docs/api/webhooks/v3.6/#incoming_chat) event scoped to all chats, and set its destination to your public address plus `/webhooks/text` — for example, `https://example.trycloudflare.com/webhooks/text`. Save it, then copy the secret Text generates into `TEXT_WEBHOOK_SECRET` in `.env`.
  </Step>

  <Step title="Start the service">
    ```shell theme={null}
    docker compose up -d --build
    docker compose logs --follow salesforce-router
    ```

    When the logs show `Salesforce chat routing ready`, confirm the health check:

    ```shell theme={null}
    curl --fail --include http://localhost:8080/healthz
    ```

    A healthy service returns HTTP `204`. On startup, the service verifies the Text property and groups. It creates any missing resources and confirms that the Salesforce integration user can read the Contact tier field. Restarts reuse resources that are already present.

    Text configuration changes can take up to 2 minutes to become readable.
  </Step>

  <Step title="Test the routing">
    Start a separate Text chat with each test Contact's email:

    * **Basic**: the chat stays with the AI Agent.
    * **Standard**: the chat moves to Regular.
    * **Enterprise**: the chat moves to Enterprise, and the customer is marked VIP — highlighted in the customer list and moved to the top of the chatting queue.

    Each chat also adds the tier to the customer's `salesforce_plan_tier` property, shown as a customer field while you're chatting with them.

    <Note>
      Transfers aren't instant. The webhook runs after the chat opens, so the AI Agent may answer first.
    </Note>

    To retest after changing a Contact's tier, close the current conversation and start a new one.
  </Step>
</Steps>

## Use tiers in reports and segments

Standard and Enterprise chats each land in their own team, which lets you break down Reports API metrics by tier. Filter response time, chat volume, or ratings by a group's ID, and the numbers reflect that tier's chats. For the cleanest numbers, reserve the Regular and Enterprise teams for this routing alone.

You can also list customers by tier because the tier is saved on each customer's profile. Use the [List customers](/docs/api/customer-data-platform/customers/list-customers) endpoint to filter by tier and combine it with other filters. For example, you can find Enterprise customers who have gone quiet or the key accounts that contact you most. A segment can also use the property to find every customer who has a tier saved.

None of this is limited to plan tier. If you adapt the service to route on another Salesforce field, such as region, each group you route to gets its own reports, and any property you save on a customer profile can be used to list customers the same way.

## Troubleshooting

| Symptom | Check |
| - | - |
| Startup says a field is missing | Confirm `SALESFORCE_PLAN_FIELD` is the exact API Name of a single-choice picklist or text field on **Contact**, and that the integration user can read it. |
| Startup says Regular or Enterprise has no eligible agents | Add human agents eligible for automatic routing — supervisor-only members don't qualify. Check `TEXT_REGULAR_AGENT_IDS` and `TEXT_ENTERPRISE_AGENT_IDS`. |
| Startup returns HTTP `403` while creating Regular or Enterprise | Confirm the PAT has `groups--all:rw` and belongs to a Text administrator, or create the missing team with eligible agents in the Text app first so startup can reuse its API group. |
| Startup says a group "must not be the default group" | `TEXT_REGULAR_GROUP_NAME` or `TEXT_ENTERPRISE_GROUP_NAME` matches your account's default chatting team. Choose a different name; the service creates a dedicated group. |
| Startup says the Customer Data Platform (CDP) property is incompatible | An existing `salesforce_plan_tier` property must have type `string` with no restriction on allowed values. |
| Salesforce returns an authentication error | Check the Consumer Key/Secret, the Client Credentials Flow execution user, the login domain, and the app's API permission and policies. |
| Salesforce says the request isn't supported on this domain | Replace `SALESFORCE_LOGIN_URL` with your organization's My Domain, not `login.salesforce.com` or `test.salesforce.com`. |
| No webhook arrives | Check the destination URL and `incoming_chat` event in the Text app, and confirm the tunnel is running. |
| A chat is skipped | Confirm the Text customer's email exactly identifies one Salesforce Contact with a nonblank tier. |

## Prepare the design for production

The service works for testing as is. Before you run it for real customers, review these two points.

<Accordion title="Make webhook handling idempotent">
  Each webhook delivery is processed independently. A repeated delivery for the same thread can repeat the tier or VIP write, or the chat transfer. If the tier or VIP write fails, the service returns an error and skips the transfer for that delivery.

  Before you deploy the service:

  * Record processed chat and thread IDs so a repeated delivery doesn't repeat the transfer.
  * Add a durable job queue so a restart doesn't drop a delivery that arrived mid-process.
</Accordion>

<Accordion title="Check human-agent membership in both teams">
  On startup, the service checks that each API group contains at least one member whose priority is not `supervisor`. This check doesn't distinguish human agents from the AI Agent. The service creates a missing group with the human agents listed in `.env`, but it doesn't add agents to or change the membership of a group that already exists.

  In the Text app, confirm that each routing team contains an eligible human agent. You must also enable the AI Agent and confirm its initial team assignment manually.

  If an AI Agent is also a member of Regular or Enterprise, its priority there determines whether it can catch a transferred chat instead of a human agent. Set its priority to supervisor in that team if you want every Standard- or Enterprise-tier chat to reach a human.
</Accordion>

***

**Keep exploring**

<p>
  <Icon icon="file" /> [Customer Data Platform API
  reference](/docs/api/customer-data-platform/)

  <br />

  <Icon icon="file" /> [Reports API reference](/docs/api/reports/)

  <br />

  <Icon icon="file" /> [Transfer
  chat](/docs/api/agent-chat/v3.6/chat-access/transfer-chat)

  <br />

  <Icon icon="file" /> [Set customer properties
  values](/docs/api/customer-data-platform/customer-properties/set-customer-properties-values)
</p>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.