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

# Salesforce Connector

> Query and analyze your Salesforce accounts, opportunities, contacts, and pipeline data directly in Ana.

## 1. Overview

The Ana x Salesforce integration connects your Salesforce org to Ana, giving you natural language access to your CRM data — accounts, opportunities, contacts, leads, and pipeline. Once configured, Ana can query deal stages, track pipeline progress, surface at-risk opportunities, and analyze sales performance directly in Ana.

<Note>
  Connect Salesforce to Ana to query and analyze your team's CRM and sales data using natural language.
</Note>

## 2. Prerequisites

You'll need:

* A Salesforce Enterprise or Developer Edition org you want Ana to query
* Admin access to create and manage a Connected App
* Python 3 installed locally (used once to generate a refresh token)
* A TextQL account with permission to add API connectors

<Tip>
  We recommend starting in a sandbox org and granting read-only scopes so you can validate the connection before pointing Ana at production data.
</Tip>

## 3. Capabilities

Once configured, Ana can:

* Query open, active, or stalled opportunities across any pipeline or sales team in plain language.
* Track how many deals are in each stage and identify which ones haven't had recent activity.
* Analyze lead sources, conversion rates, and sales performance over time.
* Get a snapshot of account and contact data, including follow-up status and engagement history.
* Surface at-risk opportunities and highlight records that need attention across your team.

## 4. Setup Instructions

***

### Step 1: Create a Salesforce Connected App

1. From your Salesforce welcome page, open the **Setup Menu** (gear icon, top right) and click **Setup for current app**.
2. Search for **App Manager** in the Quick Find menu on the top left.
3. Click **New External Connected App**.
4. Name your app and enter a valid contact email in the required field.
5. Enable OAuth and grant access to scopes. We recommend starting in a sandbox org and granting **All scopes**. Set the callback URL to `http://localhost:8080/callback` and check **Introspect all Tokens** to `True`.

<Frame caption="Enable OAuth, add the localhost:8080 callback, and grant scopes. The callback is used to fetch a refresh token in Step 2.">
  <img src="https://mintcdn.com/textql/xGAPC7KjzowMh1C7/images/connectors/salesforce/steps/oauth.png?fit=max&auto=format&n=xGAPC7KjzowMh1C7&q=85&s=08abbe01f2cfb3d9ec1b732020400260" alt="Enable OAuth settings" width="2612" height="1152" data-path="images/connectors/salesforce/steps/oauth.png" />
</Frame>

6. Configure the **Flow Enablement** and **Security** settings, then click **Save** to create the app.
7. Navigate to the **External Client App Manager** page, find your app, and open its **OAuth settings**.
8. Click **Consumer Key and Secret**, authenticate as prompted, then copy the **Consumer Key** and **Consumer Secret** — you'll need them next.

<Frame caption="Copy the Consumer Key and Consumer Secret. Treat the secret like a password.">
  <img src="https://mintcdn.com/textql/xGAPC7KjzowMh1C7/images/connectors/salesforce/steps/credentials.png?fit=max&auto=format&n=xGAPC7KjzowMh1C7&q=85&s=f09e29673be2ed6bf0c3ccc870a2f3db" alt="Consumer Key and Secret" width="1828" height="542" data-path="images/connectors/salesforce/steps/credentials.png" />
</Frame>

<Warning>
  Treat the **Consumer Secret** like a password. Don't commit it to source control or share it in plain text — paste it only into the secure field where it's needed.
</Warning>

<div style={{ height: '1rem' }} />

### Step 2: Generate a refresh token

Salesforce access tokens are short-lived, so Ana authenticates with a **refresh token**. Its validity period is determined by the settings you configured on the Connected App.

Run the following Python script, replacing `YOUR_CONSUMER_KEY` and `YOUR_CONSUMER_SECRET` with the values from Step 1:

```python theme={null}
import http.server
import urllib.parse
import webbrowser
import requests

CLIENT_ID = "YOUR_CONSUMER_KEY"
CLIENT_SECRET = "YOUR_CONSUMER_SECRET"

REDIRECT_URI = "http://localhost:8080/callback"
LOGIN_URL = "https://login.salesforce.com"


def _get_oauth_base(login_url):
    if not login_url:
        return "https://login.salesforce.com"
    if "test.salesforce.com" in login_url.lower() or ".sandbox." in login_url.lower():
        return "https://test.salesforce.com"
    return "https://login.salesforce.com"


OAUTH_BASE = _get_oauth_base(LOGIN_URL)


class CallbackHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path.startswith("/callback"):
            query = urllib.parse.urlparse(self.path).query
            params = urllib.parse.parse_qs(query)
            code = params.get("code", [None])[0]

            if code:
                token_url = f"{OAUTH_BASE}/services/oauth2/token"
                data = {
                    "grant_type": "authorization_code",
                    "code": code,
                    "client_id": CLIENT_ID,
                    "client_secret": CLIENT_SECRET,
                    "redirect_uri": REDIRECT_URI,
                }
                resp = requests.post(token_url, data=data)
                tokens = resp.json()

                self.send_response(200)
                self.send_header("Content-type", "text/html")
                self.end_headers()

                if "refresh_token" in tokens:
                    print("\n" + "=" * 50)
                    print("SUCCESS! Copy this refresh token:\n")
                    print(tokens["refresh_token"])
                    print("\n" + "=" * 50)
                    print(f"\nInstance URL: {tokens.get('instance_url')}")
                    self.wfile.write(
                        b"<h1>Success! Check your terminal for the refresh token.</h1>"
                    )
                else:
                    print(f"\nError: {tokens}")
                    self.wfile.write(f"<h1>Error</h1><pre>{tokens}</pre>".encode())
            else:
                self.send_response(400)
                self.end_headers()
                self.wfile.write(b"No code received")

    def log_message(self, format, *args):
        pass


auth_url = (
    f"{OAUTH_BASE}/services/oauth2/authorize"
    f"?response_type=code"
    f"&client_id={CLIENT_ID}"
    f"&redirect_uri={REDIRECT_URI}"
    f"&scope=api%20refresh_token"
)

print("Opening browser for Salesforce login...")
print(f"\nIf browser doesn't open, go to:\n{auth_url}\n")
webbrowser.open(auth_url)

server = http.server.HTTPServer(("localhost", 8080), CallbackHandler)
print("Waiting for callback on http://localhost:8080...")
server.handle_request()
```

The script will:

1. Open your browser to the Salesforce login page.
2. Start a local server on `localhost:8080` to receive the OAuth callback.
3. Exchange the authorization code for a refresh token.
4. Display the refresh token and instance URL in your terminal.

<Note>
  If you're using a sandbox org, update `LOGIN_URL` to your sandbox URL (e.g. `https://test.salesforce.com` or your custom domain).
</Note>

<div style={{ height: '1rem' }} />

### Step 3: Add Salesforce as an API connector in TextQL

<Frame caption="Salesforce highlighted in the TextQL API Connectors setup panel — select it to open the connector configuration form">
  <img src="https://mintcdn.com/textql/vh-E2rKQbVc2g4fF/images/datasources/salesforce/salesforce.png?fit=max&auto=format&n=vh-E2rKQbVc2g4fF&q=85&s=4a7d9b45399557ba60a6071a90ef6c15" alt="Salesforce connector in the TextQL API connectors grid" width="1750" height="1144" data-path="images/datasources/salesforce/salesforce.png" />
</Frame>

1. Go to [app.textql.com](https://app.textql.com/) and sign in.
2. In the bottom left sidebar, click **Connectors > APIs** and create a new connector:
   * **Name:** `salesforce_refresh_token`
   * **Value:** Paste the refresh token from Step 2
3. Click **Save**.
4. Paste the following prompt into Ana, replacing `YOUR_CONSUMER_KEY`, `YOUR_CONSUMER_SECRET`, and `YOUR_INSTANCE_URL` with your values. The `{{salesforce_refresh_token}}` reference is automatically replaced with your stored secret.

```
You have access to Salesforce APIs. Use these credentials:

CLIENT_ID = "YOUR_CONSUMER_KEY"
CLIENT_SECRET = "YOUR_CONSUMER_SECRET"
REFRESH_TOKEN = "{{salesforce_refresh_token}}"
INSTANCE_URL = "YOUR_INSTANCE_URL"
LOGIN_URL = "https://login.salesforce.com"

When writing Salesforce scripts, always use this pattern:

import requests

# Determine OAuth base URL
if "test.salesforce.com" in LOGIN_URL.lower() or ".sandbox." in LOGIN_URL.lower():
    oauth_base = "https://test.salesforce.com"
else:
    oauth_base = "https://login.salesforce.com"

# Get access token
resp = requests.post(
    f"{oauth_base}/services/oauth2/token",
    data={
        "grant_type": "refresh_token",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "refresh_token": REFRESH_TOKEN,
    }
)
access_token = resp.json()["access_token"]

# Query Salesforce
resp = requests.get(
    f"{INSTANCE_URL}/services/data/v60.0/query",
    headers={"Authorization": f"Bearer {access_token}"},
    params={"q": "YOUR_SOQL_QUERY"}
)
data = resp.json()
```

<Note>
  If you're using a sandbox org, update `LOGIN_URL` to `https://test.salesforce.com` or your custom sandbox domain.
</Note>

<div style={{ height: '1rem' }} />

### Step 4: Verify the connection

Confirm everything is working by running the following script locally. Fill in the values from the previous steps:

* `CLIENT_ID`: Your Consumer Key
* `CLIENT_SECRET`: Your Consumer Secret
* `REFRESH_TOKEN`: The refresh token from Step 2
* `INSTANCE_URL`: The instance URL from the script output
* `LOGIN_URL`: Your Salesforce login URL

```python theme={null}
import requests

CLIENT_ID = ""
CLIENT_SECRET = ""
REFRESH_TOKEN = ""
INSTANCE_URL = ""
LOGIN_URL = "https://login.salesforce.com"

try:
    oauth_base = "https://test.salesforce.com" if "sandbox" in LOGIN_URL.lower() else "https://login.salesforce.com"

    response = requests.post(
        f"{oauth_base}/services/oauth2/token",
        data={
            "grant_type": "refresh_token",
            "client_id": CLIENT_ID,
            "client_secret": CLIENT_SECRET,
            "refresh_token": REFRESH_TOKEN,
        }
    )
    access_token = response.json()["access_token"]
    print(f"Connected. Token: {access_token[:20]}...")

    response = requests.get(
        f"{INSTANCE_URL}/services/data/v60.0/query",
        headers={"Authorization": f"Bearer {access_token}"},
        params={"q": "SELECT Id, Name FROM Account LIMIT 3"}
    )
    records = response.json()["records"]
    print(f"Found {len(records)} accounts:")
    for r in records:
        print(f"  {r['Name']}")

    print("\nSuccess: Connection verified")
except Exception as e:
    print(f"\nFailed: {e}")
```

A successful run authenticates with Salesforce and returns a few accounts. If it fails, refer to the [Troubleshooting](#6-troubleshooting) section.

***

## 5. Usage Examples

Once configured, you can ask Ana:

* "Show me all open opportunities over \$50k that haven't been updated in more than 10 days."
* "Which deals are expected to close this quarter but have no recent activity?"
* "Find all contacts tied to accounts with no opportunity in the last 90 days."
* "How many leads came from paid search vs. organic last quarter?"
* "What's the average time an opportunity spends in each stage this year?"

<div style={{ height: '1rem' }} />

## 6. Troubleshooting

| **Symptom**                                               | **Likely Cause**                             | **Fix**                                                                                                                      |
| --------------------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `invalid_client_id` or `invalid_client`                   | Consumer Key or Secret copied incorrectly    | Re-copy the Consumer Key and Secret from the Connected App's OAuth settings with no extra spaces.                            |
| `redirect_uri_mismatch` when generating the refresh token | Callback URL doesn't match the Connected App | Ensure `http://localhost:8080/callback` is listed exactly as a callback URL on the Connected App.                            |
| `invalid_grant` when refreshing                           | Refresh token expired, revoked, or wrong org | Re-run the Step 2 script to generate a new refresh token and update the `salesforce_refresh_token` connector.                |
| Authenticated but no records returned                     | Missing scopes or querying the wrong org     | Confirm the app has the `api` scope and that `INSTANCE_URL` / `LOGIN_URL` point at the correct org (sandbox vs. production). |

## 7. Security Notes

* Follow the principle of least privilege — grant only the scopes Ana needs to query the data you care about. Read scopes are sufficient for analytics.
* Treat the Consumer Secret and refresh token like passwords. Store them only in the secure connector field and never share them in email, chat, or tickets.
* Refresh tokens remain valid based on your Connected App's session settings. Rotate them if you suspect exposure, and re-run the Step 2 script to issue a new one.
* If you no longer need the integration, revoke the Connected App in Salesforce and remove the connector from TextQL.
* For more details, refer to the [Salesforce OAuth documentation](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_flows.htm).

***

## Need Help?

For further assistance, please contact `support@textql.com`.

## Privacy Policy

For information about how we handle your data and protect your privacy, please review our [Privacy Policy](https://textql.com/privacy).
