Skip to content

Thread Control API V3

Sprinklr's Omnichannel Handover Protocol lets an external or third-party bot take part in a live customer conversation on a modern messaging channel, and then hand that conversation to Sprinklr for a human agent when appropriate.

Thread control is the mechanism that decides who is currently allowed to reply on a conversation thread: your bot, another bot, or Sprinklr itself. The Thread Control API drives that mechanism programmatically.

Before you begin. Your bot must already be integrated with Sprinklr before there is a participant to hand control to. See Omnichannel Handover Protocol.

Overview

In V3, all four control operations are served by one endpoint:

OperationMethodPath
Execute a thread control actionPOST/api/v3/thread

This is the central design change from V2, which exposed four separate endpoints. In V3 the operation you want is selected by the action field in the request body, not by the URL you call.

The four actions

action valueWhat it doesparticipantId
acquireControlAcquires control of the thread for the specified participantRequired
passControlPasses control of the thread from the current controller to the specified participantRequired
releaseControlReleases control of the thread currently held by the specified participantRequired
checkControlReturns the participant currently holding control of the threadNot used

The Sprinklr sentinel participant

participantId is not always an ID. The literal string Sprinklr is a valid participant value, and it means "the Sprinklr platform itself" — hand the conversation back to Sprinklr so a human agent, a rule, or a Sprinklr-native bot can take over.

This works in both directions:

  • Sending "participantId": "Sprinklr" passes or acquires control on Sprinklr's behalf.
  • Receiving "controllingParticipantId": "Sprinklr" in a response tells you Sprinklr holds the thread.

Treat controllingParticipantId as an opaque string. Any client that assumes it is always a 24-character hex ID will break the first time control returns to Sprinklr.

The three things that identify a control action

Every request carries the same three-part address, plus the action:

ConceptFieldExample
Which conversationentityType + entityIdCASE + a case ID
Who the action concernsparticipantIdA participant ID, or Sprinklr
What to doactionacquireControl

entityType and entityId are a pair. entityType names the kind of object the thread belongs to; entityId is that object's ID. In practice this is a case — entityType defaults to CASE.

When to use this API

Thread control can also be driven from the Sprinklr UI without any API calls, through the Rule Engine and Macros:

  • Rule conditions live under Conditions on Sprinklr Thread Handover — for example the Condition participant is condition, which can test whether the controlling participant is Sprinklr.
  • Rule actions live under Actions on Sprinklr Thread Handover — Acquire Thread Control, Pass Thread Control, and Release Thread Control.
  • On-demand rules are triggered manually through Case Macros, applied from the Details Pane in Engagement Dashboards or Agent Console.

Use this API when control decisions are made by your system — an external bot deciding it cannot answer, an orchestration layer routing between bots, or a test harness. Use the Rule Engine when the decision is made by Sprinklr-side conditions on the case.

The two approaches operate on the same underlying thread state, so a rule and an API call can contend for the same thread.

Supported messaging channels

Facebook · WhatsApp · Twitter · Instagram · Line · Kakao Talk · Apple Business Chat · Google RCS · WeChat · Sina Weibo · Viber · Sprinklr LiveChat


Base URL

https://api3.sprinklr.com/{env}/api/v3/thread

Replace {env} with your assigned environment identifier: prod, prod0, prod2, prod3, prod4, prod5, prod6, prod8, prod11, prod12, prod15, prod16, prod17, prod18, prod19, prod21, or prod25.

See APIs | Sprinklr Developer Portal for the published environment list.

Make the base URL a single configuration value so moving between environments is a configuration change, not a code change.

Environment-scoped credentials. API keys and access tokens are scoped to a single environment. A key generated for prod2 returns 401 Unauthorized against prod19 even when the request is otherwise perfectly formed. When you get an unexpected 401, verify the environment before debugging the payload.


Authentication

All calls are authenticated with OAuth 2.0. See API Overview for portal registration, API key and secret generation, and the Authorize flow. From release 26.1 onward, generate credentials through Developer Tools inside the Sprinklr platform; keys created through the legacy process remain valid.

Required headers

HeaderValuePurposeRequired on
AuthorizationBearer {{accessToken}}Authenticates the user with the serverAll requests
Key{{apiKey}}Authenticates the application with the serverAll requests
Content-Typeapplication/jsonDeclares the request body media typeAll requests
Acceptapplication/jsonDeclares the acceptable response typeRecommended

Both Authorization and Key are required on every call. Sending only one returns 401.

Credential lifetimes

CredentialLifetime
Authorization code10 minutes
API keyNo expiry
Access token30 days by default

One token pair per API key. Generating a new access token for an API key invalidates the previously issued token and refresh token for that key. If two services share one key and both refresh, they will knock each other offline. Give each service its own key.

Credential hygiene. Never commit an access token or API key to source control, never paste one into a ticket or chat, and never log the Authorization or Key header values. Every credential on this page is a placeholder.


Execute a thread control action

POST https://api3.sprinklr.com/{env}/api/v3/thread

This API differentiates behaviour entirely through the request body. The action field determines which control operation is performed.

Request body parameters

ParameterTypeRequiredDescription
actionStringYesThe thread control action to perform. One of acquireControl, passControl, releaseControl, checkControl
entityTypeStringYesThe entity type the thread belongs to. Use CASE
entityIdStringYesUnique identifier of the entity or thread the action applies to. For CASE, the case ID
participantIdStringConditionalThe participant performing or receiving the control action. Required for acquireControl, passControl, and releaseControl. Not used for checkControl. Accepts a participant ID or the literal Sprinklr

Per-action requirements

actionentityTypeentityIdparticipantId
acquireControlRequiredRequiredRequired
passControlRequiredRequiredRequired
releaseControlRequiredRequiredRequired
checkControlRequiredRequiredNot used

Validate participantId per action in your own client. Because it is unused by checkControl, it is optional at the schema level — so schema validation alone will not catch a passControl request that omits it. Only the server will.

What the four actions mean

ActionDescriptionWhat participantId names
passControlPass control of a conversation between chatbots, and from chatbot to Sprinklr and vice versa, depending on the business use caseThe participant to whom you are passing control
acquireControlAllows a participant to take control of the conversationThe participant who is taking control
releaseControlReleases control of a thread from a participant back to the primary participantThe participant who currently holds control
checkControlReturns the participant ID of the participant holding control of the conversation. Read-onlyNot used

Note the semantic asymmetry — this is the most common source of confusion. For passControl and acquireControl, participantId names the destination of control. For releaseControl, it names the current holder — the participant giving control up. Sending the wrong side of that relationship produces a request that is structurally valid and semantically wrong.

Action values are case-sensitive

action values are camelCase and exact: acquireControl, passControl, releaseControl, checkControl. Define them as constants in your own code — a typo such as aquireControl or passcontrol reaches the server and returns 400.


Worked examples

The examples below use one case and two participants, so they read as a single end-to-end sequence:

PlaceholderStands for
0000000000000000000000c1The case ID (entityId)
0000000000000000000000a1Bot A
0000000000000000000000b2Bot B

Acquire control

Request

curl -X POST \
  'https://api3.sprinklr.com/{env}/api/v3/thread' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "action": "acquireControl",
    "entityType": "CASE",
    "entityId": "0000000000000000000000c1",
    "participantId": "0000000000000000000000a1"
  }'

Response

{
  "data": {
    "controllingParticipantId": "0000000000000000000000a1"
  },
  "errors": []
}

Bot A now holds the thread.

Pass control

Pass the thread from Bot A to Bot B.

Request

curl -X POST \
  'https://api3.sprinklr.com/{env}/api/v3/thread' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "action": "passControl",
    "entityType": "CASE",
    "entityId": "0000000000000000000000c1",
    "participantId": "0000000000000000000000b2"
  }'

Response

{
  "data": {
    "controllingParticipantId": "0000000000000000000000b2"
  },
  "errors": []
}

Passing control to the participant that already holds it is a no-op. It returns 200 with the same controllingParticipantId and no error — indistinguishable from a real transfer. Compare against the previous value if you need to know whether control actually moved.

Check control

checkControl is the only action that omits participantId.

Request

curl -X POST \
  'https://api3.sprinklr.com/{env}/api/v3/thread' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "action": "checkControl",
    "entityType": "CASE",
    "entityId": "0000000000000000000000c1"
  }'

Response

{
  "data": {
    "controllingParticipantId": "0000000000000000000000b2"
  },
  "errors": []
}

Release control

Request

curl -X POST \
  'https://api3.sprinklr.com/{env}/api/v3/thread' \
  -H 'Authorization: Bearer {{accessToken}}' \
  -H 'Key: {{apiKey}}' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "action": "releaseControl",
    "entityType": "CASE",
    "entityId": "0000000000000000000000c1",
    "participantId": "0000000000000000000000b2"
  }'

Response

{
  "data": {
    "controllingParticipantId": "Sprinklr"
  },
  "errors": []
}

This is the response worth studying. controllingParticipantId is now the literal string Sprinklr, not an ID. Releasing control returned the thread to the primary participant. If your code parses controllingParticipantId as a hex ID, or uses it as a foreign key into a participant table, this response is where it fails.


Response format

All four actions return the same shape: HTTP 200 with a data object containing a single field.

FieldTypeDescription
data.controllingParticipantIdStringParticipant ID of the participant now holding control — a participant ID, or the literal Sprinklr
errorsArrayArray of error objects. Empty on success
metadataObjectResponse metadata. Treat as optional and parse defensively

Every write is also a read. Because every action returns the post-action controlling participant, acquireControl, passControl, and releaseControl each double as a checkControl. You rarely need a follow-up read to confirm the result of a write — the write already told you. Use checkControl when you need the current holder without changing it.

The error object

When errors is non-empty, each entry has this shape. All three fields are present.

FieldTypeDescription
idStringError identifier
codeIntegerNumeric error code
messageStringError key, for example case.not.found

Error responses return data as null:

{
  "data": null,
  "errors": [
    {
      "id": "0000000000000000000000e1",
      "code": 404,
      "message": "case.not.found"
    }
  ],
  "metadata": {}
}

Illustrative example.

Always inspect the errors array, not just the HTTP status. The message field is a dotted key suitable for branching. Do not branch on human-readable text.


Status codes

HTTP codeScenario
200 OKSuccess. The action was applied, or for checkControl, the current holder was read
400 Bad RequestMalformed body, unknown action value, or a required field missing
401 UnauthorizedInvalid or missing Authorization token or Key header
403 ForbiddenCaller lacks permission for the thread-control action
404 Not FoundThe entity named by entityType and entityId does not exist

Troubleshooting

SymptomLikely causeFix
401 Unauthorized with all headers presentCredentials belong to a different environmentConfirm the key and token were generated for the environment in the base URL
401 after previously workingAccess token expired (30-day default), or a second token was generated for the same API keyRefresh the token; give each service its own key
401 and only one auth header sentBoth Authorization and Key are requiredSend both
400 on a passControl callparticipantId omitted — schema validation will not catch itAdd participantId; validate per-action in your client
400 with a plausible-looking actionA typo in the action valueCheck exact spelling and camelCase: acquireControl, passControl, releaseControl, checkControl
404 Not FoundentityId does not exist, or entityType is wrong for that IDConfirm the case ID; use CASE
403 ForbiddenPermission gap, not a malformed requestCheck the user's permissions on the entity
Control appears not to changeControl was passed to the participant that already held itCompare controllingParticipantId before and after; a no-op returns the same value with no error
controllingParticipantId fails to parse as an IDThe value is the literal string SprinklrTreat controllingParticipantId as an opaque string
A rule reverted your change moments laterThe Rule Engine is also acting on this threadCheck for on-demand or automated thread-handover rules on the case

Endpoint mapping

V2V3
POST /api/v2/thread/acquire-controlPOST /api/v3/thread with "action": "acquireControl"
POST /api/v2/thread/pass-controlPOST /api/v3/thread with "action": "passControl"
POST /api/v2/thread/release-controlPOST /api/v3/thread with "action": "releaseControl"
POST /api/v2/thread/get-controlling-participantPOST /api/v3/thread with "action": "checkControl"

Request field mapping

V2 fieldV3 fieldChange
—actionNew in V3. Carries the routing decision that the URL used to carry
entityTypeentityTypeNone
entityIdentityIdNone
participantIdparticipantIdNone

This is the migration in one diff. Change the URL, add one field:

- POST https://api3.sprinklr.com/{env}/api/v2/thread/acquire-control
+ POST https://api3.sprinklr.com/{env}/api/v3/thread

  {
+   "action": "acquireControl",
    "entityType": "CASE",
    "entityId": "0000000000000000000000c1",
    "participantId": "0000000000000000000000a1"
  }

Response parsing does not need to change

V2V3Change
data.controllingParticipantIddata.controllingParticipantIdNone
errorserrorsNone

The response contract is compatible with V2 for every action.

The error-handling change to plan for

This is the migration's genuine risk, and it is invisible in the field mapping.

In V2, the action lived in the URL. A wrong or misspelled action produced a wrong URL, and the platform returned 404 Not Found at the routing layer — before any business logic ran. Your monitoring saw a 404 on a path you could read straight out of the log line.

In V3, the action lives in the body. A misspelled action now reaches a valid, existing endpoint and fails inside the handler. You see a 400, not a 404, and the log line shows POST /api/v3/thread — identical to every successful call. The URL no longer tells you which operation was attempted.

Consequences to plan for:

  1. Alerting on URL patterns stops working. If you alert or dashboard on /thread/pass-control versus /thread/release-control, those signals collapse into one path. Log the action value explicitly on every call.
  2. 404 and 400 semantics change. In V2, 404 usually meant "bad URL." In V3, 404 means "the case does not exist" — a genuine business condition — and 400 covers the malformed-action case. Retry and alerting logic keyed to those codes needs review.
  3. Client-side action validation is now yours. Add an enum or constant set in your own code.

Hand off from your bot to a human agent

Your bot is the primary responder on WhatsApp. It detects an intent it cannot handle and needs a Sprinklr agent.

{
  "action": "passControl",
  "entityType": "CASE",
  "entityId": "0000000000000000000000c1",
  "participantId": "Sprinklr"
}

The response confirms "controllingParticipantId": "Sprinklr". Your bot should stop replying on this thread until it sees control return.

Take control at the start of a conversation

A new case opens and your bot should own the first turns.

{
  "action": "acquireControl",
  "entityType": "CASE",
  "entityId": "0000000000000000000000c1",
  "participantId": "0000000000000000000000a1"
}

Return the thread to the primary participant

Your bot completed its flow and should relinquish the thread rather than hand it to a named destination.

{
  "action": "releaseControl",
  "entityType": "CASE",
  "entityId": "0000000000000000000000c1",
  "participantId": "0000000000000000000000a1"
}

Note the argument: participantId is the bot giving up control, not the recipient. The response returns "Sprinklr" — the primary participant that now holds the thread.

releaseControl versus passControl to Sprinklr. Both end with Sprinklr holding the thread, so it is easy to treat them as interchangeable. They are not: releaseControl says "I am done" and takes your own ID; passControl says "you take it" and takes the destination's ID. Use releaseControl when your bot is finishing normally, and passControl when you are making a routing decision toward a specific destination.

Guard every outbound message

To be certain your bot never talks over an agent, call checkControl before sending:

{
  "action": "checkControl",
  "entityType": "CASE",
  "entityId": "0000000000000000000000c1"
}

Send only if data.controllingParticipantId equals your own participant ID.

This costs one extra round trip per message. For high-volume bots, prefer to track control state from the responses you already receive — every write action returns the new holder — and use checkControl only to resynchronise after an error or a gap.

Route between bots

A triage bot classifies the request and routes it to a specialist bot. Control passes between chatbots, and from chatbot to Sprinklr and vice versa.

{
  "action": "passControl",
  "entityType": "CASE",
  "entityId": "0000000000000000000000c1",
  "participantId": "0000000000000000000000b2"
}

Reconcile state after a failure

Your bot crashed mid-conversation and does not know whether it still holds control.

Call checkControl on each open case and branch on the result. This is the cheapest way to rebuild control state without mutating anything — it is the only action of the four that cannot change the thread.

Detect a Rule Engine hand-off

A Sprinklr on-demand rule, triggered by an agent applying a Case Macro, took control away from your bot. Your bot did not make that call, so nothing in its own request log explains the change.

Poll checkControl, or check control before each send, and treat a controllingParticipantId that is neither your ID nor your last known value as an external hand-off.

Build a regression harness

The four actions form a natural round trip on a single case:

  1. checkControl — record the starting holder
  2. acquireControl with Bot A — assert controllingParticipantId is Bot A
  3. passControl to Bot B — assert it is Bot B
  4. checkControl — assert it is still Bot B
  5. releaseControl with Bot B — assert it is Sprinklr

Because every action returns the resulting holder, each step is self-asserting; no separate read is needed between steps.


Best practices

Treat controllingParticipantId as an opaque string

  • It is not always a 24-character hex ID. The literal Sprinklr is a valid value and is what releaseControl returns.
  • Do not use it as a database foreign key without handling the sentinel.
  • Do not regex-validate it as hex.
  • The same applies on the request side: Sprinklr is a valid participantId to send.

action correctness is your responsibility

  • Values are camelCase and case-sensitive: acquireControl, passControl, releaseControl, checkControl.
  • Define them as constants once. A typo reaches the server and returns 400.

Know which side participantId names

  • passControl and acquireControl — the participant that receives control.
  • releaseControl — the participant that currently holds control and is giving it up.

Every write is also a read

  • All four actions return the post-action controllingParticipantId. You almost never need a follow-up checkControl after a write.

checkControl is a read behind a POST

  • It does not modify the thread, but it is still an HTTP POST: not safe, not cacheable, and not retryable by any intermediary that assumes GET semantics.
  • Do not put it behind an HTTP cache.

Plan for concurrency

  • The Rule Engine, Macros applied by agents, and other API clients act on the same thread state. A control decision can be overridden a moment after you make it.
  • Check control immediately before acting, not once at the start of a long flow.
  • No optimistic-concurrency mechanism is exposed on this endpoint, so a lost update is possible and not detectable from the response alone.

Repeated actions are silent no-ops

  • Re-sending acquireControl for a participant that already holds control returns 200 with the same controllingParticipantId and no error. Compare against the previous value if you need to know whether anything moved.

Log the action value

  • The URL no longer distinguishes operations, and without the logged action your logs cannot tell you what was attempted.