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

# Salesforce troubleshooting

> Debug Salesforce connection, case creation, live messaging handoff, and agent reply sync issues.

Before debugging, have this ready:

* Admin access to both your Salesforce org and your [OpenCX dashboard](https://platform.open.cx/settings/integrations).
* A specific session ID (from [Inbox](https://platform.open.cx/inbox)) or Salesforce Case ID where the problem shows.
* The webhook URL shown in [Settings → Integrations](https://platform.open.cx/settings/integrations) → Salesforce (for email issues).

<Tip>
  Check connection status in [**Settings → Integrations**](https://platform.open.cx/settings/integrations) → Salesforce as your first step. Most issues are credential or configuration related.
</Tip>

## Email Cases

Jump to the symptom that matches what you're seeing.

### **"OAuth credentials won't save"**

<Tooltip tip="Usually caused by the Connected App not being active yet, or credentials copied with extra whitespace.">Likely cause</Tooltip>: Connected App still activating, or credentials copied incorrectly.

**Fix:** wait 2–10 minutes after creating the Connected App, then retry. Re-copy the Consumer Key and Consumer Secret without leading or trailing spaces. Confirm the Login URL is `https://login.salesforce.com` (production) or `https://test.salesforce.com` (sandbox).

### **"Connect to Salesforce fails"**

<Tooltip tip="The callback URL in the Connected App doesn't match what OpenCX sends, or the browser is blocking the popup.">Likely cause</Tooltip>: callback URL mismatch or popup blocked.

**Fix:** in Salesforce, open the Connected App and confirm the callback URL is exactly:

```
https://api.open.cx/backend/salesforce-case-management/oauth/callback
```

Allow popups for `platform.open.cx` in your browser.

### **"OAUTH\_APPROVAL\_ERROR\_GENERIC after logging in to Salesforce"**

Salesforce shows *"We can't authorize you because of an OAuth error … OAUTH\_APPROVAL\_ERROR\_GENERIC : An unexpected error has occurred during authentication."*

<Tooltip tip="Salesforce rejects an authorize request naming any scope the app does not have selected, and reports it as this one opaque error.">Likely cause</Tooltip>: a scope mismatch between what OpenCX requests and what the app grants. The common case: the app is an **External Client App** (which cannot grant `Full access (full)`) while the OpenCX workspace still requests `full` — the historical default.

**Fix:**

1. Using an External Client App? Ask OpenCX to switch your workspace to the least-privilege scope set (`api id refresh_token offline_access`), and select exactly the three matching checkboxes on the app — see [Create an External Client App](/integrations/salesforce/email-cases#setup).
2. Using a classic Connected App? Make sure every scope OpenCX requests is selected on the app (`Full access` + `Perform requests at any time` for the default configuration).
3. If you recreated the app, paste the **new** Consumer Key and Secret into OpenCX and save — retrying with the old app's saved credentials keeps failing the same way. Wait 2–10 minutes after app creation, then **Save and Connect** again.

### **"error=invalid\_client\_id when redirected to Salesforce"**

<Tooltip tip="Salesforce doesn't recognize the consumer key yet — new apps take a few minutes to propagate to the OAuth endpoints.">Likely cause</Tooltip>: the app was created moments ago, or the Consumer Key was copied incorrectly.

**Fix:** wait 2–10 minutes and retry. If it persists, re-copy the Consumer Key (no whitespace) and confirm the Login URL points at the org that owns the app — for a sandbox use `https://test.salesforce.com`, or use your org's My Domain URL directly.

### **"OAUTH\_AUTHORIZATION\_BLOCKED — Cross-org OAuth flows are not supported for this external client app"**

<Tooltip tip="External Client Apps are local to the org that created them; a login that lands in any other org is rejected before scopes are ever evaluated.">Likely cause</Tooltip>: you authorized into a different Salesforce org than the one that owns the app. A generic Login URL (`https://test.salesforce.com` or `https://login.salesforce.com`) sends you to whichever org your credentials resolve to, which need not be the app's org.

**Fix:**

1. Set the **Login URL** to the **My Domain URL of the org that owns the app** — for example `https://yourorg--sandboxname.sandbox.my.salesforce.com`. This pins the flow to one org and is the single most common fix.
2. If it still fails, the app genuinely lives elsewhere. An External Client App cannot be reused across orgs unless it is packaged and distributed, so create the app **inside** the org you are connecting.
3. Leaving the Login URL blank silently defaults to **production** — a frequent cause when you meant to connect a sandbox.

### **"Connection works, then stops a few hours later"**

Cases stop creating sessions and rep replies stop syncing. OpenCX logs `Unable to refresh session due to: expired access/refresh token`, and reconnecting fixes it only until the next token expiry.

<Tooltip tip="The access token lasts a couple of hours; if the refresh token is expired or single-use, there is no way back and every call fails until someone reconnects by hand.">Likely cause</Tooltip>: the app's refresh token policy expires the refresh token.

**Fix:** in the app's OAuth policies, set the **Refresh Token Policy** to **Refresh token is valid until revoked**, then reconnect once. Also confirm the app grants **Perform requests at any time** (`refresh_token`, `offline_access`) — without it no usable refresh token is ever issued.

### **"Case comments and replies are authored by the wrong Salesforce user"**

<Tooltip tip="The seat is whoever completed the OAuth connection — the connect flow reuses your active Salesforce browser session, so it is easy to re-authorize yourself by accident.">Likely cause</Tooltip>: the integration is connected as the wrong user.

**Fix:** log out of Salesforce (or use a private window), sign in as the user you want, then **Save and Connect** again. Full steps and the requirements that user must meet are in [Changing the integration user](/integrations/salesforce/email-cases#changing-the-integration-user-the-ais-salesforce-seat).

### **"Case created in Salesforce but no session appears in OpenCX"**

<Tooltip tip="The Apex callout never reaches OpenCX — usually a Named Credential whose Name doesn't match the callout: reference in the Apex class, or a wrong webhook URL.">Likely cause</Tooltip>: the `@future` webhook callout is failing inside Salesforce.

**Fix:**

1. **Setup → Apex Jobs** — find the failed future jobs from your test; the error column names the exact cause.
2. **Setup → Named Credentials** — the **Name** must exactly match the `callout:` reference in the Apex class (`OpenCX_Integration` by default), and the URL must be the **base** Webhook URL from the OpenCX dashboard: everything up to and including `/webhook`, with the token segment removed.
3. **The token must reach us as a header.** Confirm the Apex class sets `x-opencx-token` (or `Authorization: Bearer`) and that the value it reads is populated — an empty protected Custom Setting yields a blank header and the webhook is rejected with `Missing webhook token`. A token left in the URL path still works, but it leaks into debug logs and Apex Jobs entries; move it to the header.
4. Passing Apex tests do **not** prove this path — tests mock the callout (`HttpCalloutMock`), so a broken Named Credential still shows green tests.

### **"Emails to my support address never create a Case"**

<Tooltip tip="Mail is dropped before Salesforce accepts it — forwarding unverified or routing address unverified.">Likely cause</Tooltip>: the forward from your support mailbox to the long `...case.salesforce.com` address isn't confirmed, or the Routing Address isn't verified.

**Fix:** in **Setup → Email-to-Case → Routing Addresses**, the address must show **Verified**. Then confirm the mailbox forward itself (e.g. Gmail's forwarding confirmation — the code lands in Salesforce as an EmailMessage; see Email Cases Step 3). Emails sent directly to the long Email Services Address bypass forwarding — if those create Cases but your support address doesn't, the forward is the broken link.

### **"Handoff runs but no Case appears in Salesforce"**

<Tooltip tip="The Apex trigger isn't deployed, the Named Credential URL is wrong, or the webhook token has expired.">Likely cause</Tooltip>: Apex trigger not deployed or inactive, Named Credential misconfigured.

**Fix:**

1. In Salesforce **Setup → Apex Triggers**, confirm `CaseTrigger` is active.
2. In **Setup → Named Credentials**, confirm `OpenCX_Integration` points to the base webhook URL from OpenCX — no token segment (re-copy if it looks stale).
3. In **Setup → Apex Classes**, confirm `OpenSalesforceCaseManagement` is saved and active.

### **"AI replies show as sent in OpenCX but the contact never receives the email"**

<Tooltip tip="The OpenCxOutboundEmailSender trigger is missing from your Salesforce org, or Email Deliverability is set to 'System email only'.">Likely cause</Tooltip>: outbound delivery trigger not deployed, or org-wide email deliverability blocked.

A plain `EmailMessage` insert in Salesforce creates a record in the Case Feed but does not actually transmit the email — transmission requires `Messaging.sendEmail()` via the `OpenCxOutboundEmailSender` trigger. If the trigger is missing, the AI reply logs to the Case Feed (showing Status `Sent`) but never leaves Salesforce.

**Fix:**

1. In Salesforce **Setup → Apex Triggers**, confirm `OpenCxOutboundEmailSender` exists, is **Active**, and targets `EmailMessage`. If missing, deploy it per [Email Cases → Step 3, Trigger 5](/integrations/salesforce/email-cases#apex-triggers).
2. In **Setup → Email → Deliverability**, confirm **Access level** is `All email` (not `System email only` or `No access`).
3. Confirm the integration user's profile has the **Send Email** permission.
4. Re-send the AI reply from OpenCX. It should now land in the contact's inbox.

Run `SELECT Id, CreatedDate, FromAddress, ToAddress, Status FROM EmailMessage WHERE ParentId = '<caseId>' AND Incoming = false ORDER BY CreatedDate DESC` to confirm outbound records exist in Salesforce. If they do but the contact still doesn't receive the email, enable a **Debug Log** for the integration user (**Setup → Debug Logs → New** with the integration user as Traced Entity), re-send the reply from OpenCX, then open the resulting log and look for the `EmailMessage` insert transaction — confirm `OpenCxOutboundEmailSender` executed and didn't throw. If it didn't run, double-check in **Setup → Apex Triggers** that the trigger is still **Active**.

### **"Customer replies open a duplicate Case instead of threading into the original"**

<Tooltip tip="The outbound trigger isn't injecting Salesforce's Lightning-Threading token, so Email-to-Case can't match replies to the existing Case.">Likely cause</Tooltip>: `OpenCxOutboundEmailSender` is missing the call to `EmailMessages.getFormattedThreadingToken(em.ParentId)`, or an older version of the trigger is deployed.

**Fix:** update `OpenCxOutboundEmailSender` to the version documented at [Email Cases → Trigger 5](/integrations/salesforce/email-cases#apex-triggers). The current version appends the threading token to both the subject and the body, which is what Email-to-Case (Summer '22+) uses to attach customer replies to the same Case.

You can confirm by opening a recent outbound `EmailMessage` record on the Case Feed and checking its Subject/Body — you should see a `ref:!...:ref` token at the end. If the token isn't there, the trigger is stale.

### **"Customer replies never reach Salesforce at all"**

<Tooltip tip="Replies are going back to the integration user's personal Salesforce inbox because setReplyTo isn't configured, or the forwarding / Routing Address isn't registered with Email-to-Case.">Likely cause</Tooltip>: `REPLY_TO_ADDRESS` not set on the outbound trigger, or the reply-to address isn't registered as a verified Routing Address.

**Fix:**

1. In `OpenCxOutboundEmailSender`, confirm `REPLY_TO_ADDRESS` is set to your customer-facing forwarding address (e.g. `support@yourcompany.com`). Without `setReplyTo()`, replies go to the integration user's personal Salesforce inbox and never reach Email-to-Case.
2. In **Setup → Service → Email-to-Case → Routing Addresses**, confirm that same address is listed and its status is **Verified**. Until verified, Salesforce drops forwarded mail silently.
3. Confirm the forwarding itself works: send a plain email to your forwarding address, then check **Setup → Email Logs** — if no delivery record appears within 30 minutes, the forward is failing before it reaches Salesforce (usually SPF / DMARC). Publish an SPF record on your domain that includes `_spf.salesforce.com`, or switch to an SRS-aware forwarder.

### **"Hit `SINGLE_EMAIL_LIMIT_EXCEEDED` in the Debug Log"**

<Tooltip tip="Messaging.sendEmail has a hard per-org daily cap of 15/day on Developer/trial orgs and 5,000/day on Enterprise/Unlimited. Once exceeded, every subsequent send is rejected until midnight GMT.">Likely cause</Tooltip>: the org's external email cap for the GMT day is exhausted.

**Fix:**

1. Wait for midnight GMT — the counter resets automatically, no code change required.
2. If you're doing heavy iteration in a Developer / trial org (15/day cap), move testing to an Enterprise/Unlimited sandbox (5,000/day). The limit is a hardcoded edition characteristic — it cannot be raised on Dev Edition.
3. For high-volume production workloads that exceed 5,000/day, replace `Messaging.sendEmail` in `OpenCxOutboundEmailSender` with an HTTP callout to an external ESP (SendGrid / Postmark / Mailgun) via a Named Credential.

Note that `setTargetObjectId(contactId)` **does not** bypass this limit for external recipients — that optimisation only applies when the target is a Salesforce User record.

### **"Gmail forwarding stuck at `Unverified`"**

<Tooltip tip="Gmail sends a verification code to the Salesforce inbound address. Because Salesforce captures it as a Case, you need to pull the code out of Salesforce before pasting it back into Gmail.">Likely cause</Tooltip>: Gmail's forwarding handshake sent a verification code to Salesforce's long `@...case.salesforce.com` address; Salesforce received it as a new Case but you didn't retrieve the code to paste back into Gmail.

**Fix:**

1. In Gmail → **Settings → Forwarding and POP/IMAP** → click **Re-send email** next to the pending Salesforce address (sends a fresh code).

2. In Developer Console → Query Editor:

   ```sql theme={"dark"}
   SELECT Id, Subject, TextBody, CreatedDate
   FROM EmailMessage
   WHERE FromAddress = 'forwarding-noreply@google.com'
   ORDER BY CreatedDate DESC
   LIMIT 1
   ```

3. Open the newest row and copy the 9-digit confirmation code from the `TextBody`.

4. Paste the code into Gmail's verification input → **Verify**. Status should flip to `Confirmed` and forwarding is live.

The same pattern works for other mail providers (Outlook, FastMail, custom domains) — send a fresh verification, then pull the code from the newest `EmailMessage` in Salesforce.

### **"Rep replies don't reach the contact"**

<Tooltip tip="The Apex trigger isn't firing on case events, or the webhook URL is stale.">Likely cause</Tooltip>: webhook not firing or Named Credential URL outdated.

**Fix:** re-copy the webhook URL from [Settings → Integrations](https://platform.open.cx/settings/integrations) → Salesforce and update the Named Credential. Confirm the Apex trigger fires on Case `after insert`.

## Live Messaging

### **"MIAW settings won't save"**

<Tooltip tip="Instance URL is missing https://, or PEM keys are missing their header/footer lines.">Likely cause</Tooltip>: Instance URL format wrong or PEM encoding incomplete.

**Fix:** include `https://` in the Instance URL (e.g. `https://yourcompany.my.salesforce.com`). Ensure PEM keys include the `-----BEGIN PUBLIC KEY-----` and `-----BEGIN PRIVATE KEY-----` header and footer lines.

### **"Handoff fails with access token error"**

<Tooltip tip="The public/private key pair doesn't match, the Auth Key ID is wrong, or the key isn't registered in the Embedded Service Deployment.">Likely cause</Tooltip>: key mismatch or wrong Auth Key ID.

**Fix:** regenerate the RSA key pair, re-register the public key in your Salesforce Embedded Service Deployment's Auth Key settings, copy the new Auth Key ID, and update all six fields under **In-App Chat (MIAW)** in OpenCX.

### **"Conversation created but no messages appear"**

<Tooltip tip="The ES Developer Name is wrong (it's case-sensitive) or Omni-Channel isn't configured for the Embedded Service Deployment.">Likely cause</Tooltip>: ES Developer Name mismatch or Omni-Channel not set up.

**Fix:** in Salesforce **Setup → Embedded Service Deployments**, copy the exact **API Name** (case-sensitive) and paste it into the ES Developer Name field in OpenCX. Confirm that Omni-Channel routing is configured for this deployment.

### **"Routing attributes missing in Salesforce"**

<Tooltip tip="Omni-Channel flow doesn't reference the attribute names OpenCX sends.">Likely cause</Tooltip>: Omni-Channel flow not reading the attributes.

**Fix:** in Salesforce **Setup → Omni-Channel Flows**, verify your flow references the attribute names (`OpencxSessionId`, `Chat_Consumer_Type`, `Origin`, `Priority`, `Chat_language`). Attributes are only useful if your flow reads them.

### Still stuck?

If none of the above resolves the issue, open a support request with:

* The session ID from [Inbox](https://platform.open.cx/inbox).
* The error message or unexpected behavior.
* Screenshots of your Salesforce setup (Named Credential, Apex Trigger status, or MIAW Embedded Service Deployment).

## Limits & timing

|                              | Value                                          |
| ---------------------------- | ---------------------------------------------- |
| **Connected App activation** | 2–10 minutes after creation                    |
| **Handoff retries**          | 3 attempts with increasing delays              |
| **Transcript length**        | Up to 4,000 characters per handoff message     |
| **Private key display**      | Redacted after first save — re-enter to change |

***

## Related Documentation

<CardGroup cols={2}>
  <Card title="Salesforce overview" icon="cloud" href="/integrations/salesforce/overview">
    Capabilities, supported channels, observability.
  </Card>

  <Card title="Email Cases" icon="envelope" href="/integrations/salesforce/email-cases">
    Re-verify OAuth, webhook, and Apex setup.
  </Card>

  <Card title="Live Messaging" icon="messages" href="/integrations/salesforce/live-messaging">
    Re-verify JWT credentials and routing attributes.
  </Card>

  <Card title="Handoff settings" icon="user-group" href="/handoff/introduction">
    Global handoff rules and office hours.
  </Card>
</CardGroup>
