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

# Salesforce Integration Reference

> Backend callback contracts, recovery, and optional Salesforce configuration for the Trackly browser SDK.

Start with [Connect Your Salesforce Dialer](/api-reference/voice/salesforce).
This reference covers the backend contract and configuration details you may
need while adapting an existing integration. Apex, LWC, and Open CTI are options
for your current architecture, not prerequisites to build a new dialer.

## Backend callbacks

All Trackly paths below are relative to
`https://api.tracklysms.com/api/v1/voice`. Send the server-held key as
`Authorization: Bearer <Trackly API key>`. Reads require `voice_calls.read`;
creation and cleanup require `voice_calls.write`.

Callbacks resolve the parsed response envelope. On failure, reject an error
carrying the safe Trackly `code`; retain HTTP status and `Retry-After` for your
backend's handling. Honor the supplied `AbortSignal` where your transport permits,
use bounded request deadlines, and disable automatic mutation retries.

| Callback                                      | Trackly request                                                                                                            | Response                                                              |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `createSession({signal})`                     | `POST /browser-sessions`, body `{externalSubject}` derived from the authenticated user; server-managed `Idempotency-Key`.  | `201 {session, transport}`.                                           |
| `getCurrentSession({signal})`                 | `GET /browser-sessions?externalSubject=...`, deriving the subject server-side.                                             | `200 {session, call}` or `{session:null, call:null}`; no credentials. |
| `createCall(input, {idempotencyKey, signal})` | `POST /browser-calls`, body `{sessionId, from, to, maxDurationSeconds?, record?, customData?}`; forward `Idempotency-Key`. | `202 {call}`; admission is not telephone answer.                      |
| `getCall(id, {signal})`                       | `GET /browser-calls/{id}` after user authorization.                                                                        | `200 {call}`; preserve all identity and revision fields.              |
| `hangupCall(id, {signal})`                    | `POST /browser-calls/{id}/hangup`, JSON `{}`.                                                                              | `202 {call}`; cancellation is not yet terminal evidence.              |
| `endSession(id, {signal})`                    | `DELETE /browser-sessions/{id}`, JSON `{}`.                                                                                | `200 {session}`; revocation requests cleanup.                         |

`record` is a boolean, defaults to `false`, and requests recording when supported
for the account. `customData` accepts up to 32 string pairs: keys contain 1–128
characters, cannot start with `$` or contain a null character, and values contain
at most 1,024 characters. Its compact UTF-8 JSON must fit within 8,192 bytes.
Pass either option in the input to `startCall()`; the SDK forwards it unchanged.

`GET /browser-sessions/{id}` also returns `{session}` metadata for authorized
recovery. Session metadata includes `id`, `agentId`, `externalSubject`, `status`,
`expiresAt`, and `backend`. The successful issuance `transport` contains temporary
SIP registration and ICE configuration. Pass that complete envelope to the SDK;
metadata-only recovery cannot substitute for it.

### Adapter example

These `/integration/voice/*` paths are example routes in **your existing backend**,
not Trackly endpoints. Implement them using the request table above.
`authenticatedJson` stands for your existing authenticated request helper; it must
apply your CSRF/origin protections, return parsed JSON, reject errors with `code`,
and pass through the signal without automatically retrying mutations.

```js theme={null}
function makeTracklyBackend(authenticatedJson) {
  const pathId = (id) => encodeURIComponent(id);
  return {
    createSession: ({ signal }) =>
      authenticatedJson("POST", "/integration/voice/sessions", {}, { signal }),
    getCurrentSession: ({ signal }) =>
      authenticatedJson("GET", "/integration/voice/sessions/current", undefined, { signal }),
    createCall: (input, { idempotencyKey, signal }) =>
      authenticatedJson("POST", "/integration/voice/calls", input, {
        signal, headers: { "Idempotency-Key": idempotencyKey },
      }),
    getCall: (id, { signal }) =>
      authenticatedJson("GET", `/integration/voice/calls/${pathId(id)}`, undefined, { signal }),
    hangupCall: (id, { signal }) =>
      authenticatedJson("POST", `/integration/voice/calls/${pathId(id)}/hangup`, {}, { signal }),
    endSession: (id, { signal }) =>
      authenticatedJson("DELETE", `/integration/voice/sessions/${pathId(id)}`, {}, { signal }),
  };
}
```

### Ownership and retry rules

* Derive a stable `externalSubject`, up to 200 characters, from authenticated org
  and user IDs. Scope mappings by client business when serving multiple clients.
  Check that every requested session/call belongs to that user; account/key
  authentication alone does not isolate users sharing a key.
* Use the same issuing API key for an agent and its sessions. Plan key rotation
  with Trackly because a new issuer does not inherit existing browser resources.
* Persist session/call IDs, their owner, attempt keys, original inputs, and
  unresolved outcomes. Serialize session issuance across tabs and backend workers.
* Session and call creation require a nonblank `Idempotency-Key` of at most 128
  characters. Changed input with the same key returns `409 idempotency_conflict`.
  A lost response retains the original attempt; do not generate a new key to retry.
* Authorize caller/contact selection. Verify returned media URLs against
  Trackly's approved transport configuration. Preserve approved WSS/ICE settings;
  do not add browser-selected relays or credentials.
* Keep credentials in memory and return them with `Cache-Control: no-store`.
  Do not log API keys, SIP passwords, TURN credentials, full session envelopes,
  or SDP. Keep call/session reads uncached because fresh identity checks authorize
  incoming media and establish cleanup.

## Status, expiry, and cleanup

The browser projection uses `waiting_agent`, `dialing_recipient`, `connected`,
`ending`, `ended`, `failed`, and `outcome_unknown`. It differs from the generic
Voice API's individual-leg state names. Use `call.state: "connected"` for the
telephone conversation and `busy: false` before another attempt.

The earlier session/registration expiry blocks new calls. An accepted call keeps
its own configured duration limit; renewal does not extend it. Use `renew()` only
while idle. Provide an explicit playback action if `audio_playback_blocked` occurs.
Mute affects outgoing microphone audio only; DTMF accepts one `0–9`, `*`, `#`, or
uppercase `A–D` digit. An uncertain keypad result is not automatically resent.

On a fresh helper, `recover()` restores metadata and cleanup controls, not live
audio or credentials. A discovered session sets `recovery_required`. Request
explicit cleanup using `disconnect()` and wait for resolution before connecting.
An empty metadata result cannot erase an earlier unknown mutation in your backend.

Local hangup, HTTP 202, session revocation, and an ended browser audio stream are
not individually proof that both call legs have stopped. Preserve `busy` and
unknown states until matching terminal reads and SDK cleanup resolve them.
Revoke sessions server-side on logout; browser unload callbacks may never arrive.
There is no automatic redial or audio reattachment after reload/network failure.

## Quotas and support

The SDK reads an active call approximately once per second. Ten active agents
need roughly 600 reads/minute before admission and cleanup. The default key quota
is 100 requests/minute and 10,000/hour; arrange an appropriate quota with Trackly
before a multi-agent test. Do not add another poller. Inspect `X-RateLimit-Limit`,
`X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` on HTTP 429.

| Error                                                | Action                                                                                   |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `browser_voice_unavailable`                          | Trackly must verify account activation and media availability.                           |
| `browser_agent_busy` / `browser_session_unavailable` | Recover the existing session; check expiry and the issuing key.                          |
| `compliance_*`                                       | Present the policy rejection and stop that attempt.                                      |
| `connection_failed`                                  | Check selected devices, microphone permission, session expiry, and the exact WSS policy. |
| `call_outcome_unknown` / `cleanup_unknown`           | Retain the original IDs and attempt; reconcile without redial.                           |

For support, retain UTC time, application attempt ID, Trackly session/call IDs,
safe error code, browser version, and network type. Optional `onTiming` callbacks
report bounded setup durations without credentials or network addresses.

## Salesforce packaging

Use the section matching your existing component. These are supported Salesforce
configuration mechanisms; the preview bundle still needs testing inside your
actual org's security settings and browser environment.

### Existing LWC component

Upload the downloaded JavaScript bundle as a Static Resource, then load it once
using `loadScript` from `lightning/platformResourceLoader`. Use your component's
audio element and existing handlers. Salesforce documents this in
[Use Third-Party JavaScript Libraries](https://developer.salesforce.com/docs/platform/lwc/guide/js-third-party-library).

```js theme={null}
import voiceBundle from "@salesforce/resourceUrl/tracklyVoice";
import { loadScript } from "lightning/platformResourceLoader";

async loadTrackly() {
  await loadScript(this, voiceBundle);
  const remoteAudio = this.template.querySelector("audio");
  this.voice = window.TracklyVoice.createVoiceBrowser({
    backend: this.tracklyBackend,
    remoteAudio,
    onStatus: (state) => this.renderCallStatus(state),
  });
}
```

This is a fragment inside your existing component class. Guard repeated renders
so it loads and initializes once. LWS supplies a sandboxed `window`; load and use
the SDK in the same component namespace. Keep LWS/Locker enabled and validate
WebSocket, microphone, and audio behavior in the org. See
[third-party library considerations for LWS](https://developer.salesforce.com/docs/platform/lightning-components-security/guide/lws-js.html).

### Existing Visualforce or Open CTI wrapper

Load the bundle through your current approved static-resource/application setup
and retain your existing Salesforce screen-pop and activity-logging integration.
The SDK supplies calling behavior; it does not register a Salesforce Call Center.
Salesforce lists Open CTI as maintenance-only, with retirement scheduled for
February 2028 and restrictions on newly created Agentforce Service orgs. Reusing
an existing eligible wrapper is a separate decision from creating a new one.
See the [Open CTI support policy](https://developer.salesforce.com/docs/service/api-cti/guide/sforce-api-cti-support.html).

### Media permissions

Approve Trackly's exact `wss://` media origin under the applicable `connect-src`
policy, and your backend's HTTPS origin if the browser calls it directly. An HTTPS
entry does not approve a different WSS URL. Microphone permission is separate:
embedded components need permission through the parent/iframe policy as well as
the browser's user prompt. Test both the selected microphone and output device.
See Salesforce's [CSP guide](https://developer.salesforce.com/docs/platform/lightning-components-security/guide/content-security-policy-intro.html).

### Apex instead of an existing application backend

If your current dialer already uses Apex for server calls, you can keep it.
Configure a modern Named Credential with a Custom External Credential and a Named
Principal for the Trackly API key. Supply the custom Bearer header server-side;
grant principal access through your permission set. The browser must not receive
that persistent key. Salesforce documents
[custom API-key headers](https://help.salesforce.com/s/articleView?id=sf.nc_custom_headers_and_api_keys.htm\&language=en_US\&type=5).

Implement the same user ownership and durable attempt rules above. Do not mark
credential or call-control methods cacheable. A cancelled browser request cannot
recall an Apex callout already sent to Trackly; preserve the attempt for recovery.
Check Apex limits at the intended per-agent polling volume before rollout.
