# CFM Transaction APIs V3 — Developer Guide

- **Applies to:** Sprinklr Customer Feedback Management (CFM) Transaction APIs V3 (`/api/v3/surveyTransactions`)
- **V2 API reference:** [CFM Transaction APIs | Sprinklr Developer Portal](https://dev.sprinklr.com/cfm-transaction-apis)
- **Module overview:** [Customer Feedback Management | Sprinklr Developer Portal](https://dev.sprinklr.com/customer-feedback-management)
- **Jira reference:** [IN-12886 — CRUD for Custom Feedback Management APIs](https://sprinklr.atlassian.net/browse/IN-12886) (Story · status **Ready For Merge** · fix version **26.10**, release date **2026-09-07** · epic **IN-12622 — V3 parity APIs all entities**)
- **OpenAPI tag:** `Survey Transactions V3` — *"Manage survey transactions used to trigger and track survey invitations. Supports adding transactions to a transaction group, fetching a transaction by ID, and deleting a transaction."*


## 1. Overview

A **transaction** is a CFM-specific record that documents a single customer interaction at a specific moment in time — a purchase, a refund, a service visit, a support contact, or a loyalty redemption. Transactions are contained within a user profile and carry a unique set of metadata stored as key-value pairs in **Transaction Fields**. Because each interaction is stored independently rather than overwriting the profile, a customer who shops twice produces two transactions, and each one can drive its own survey distribution.

Transactions are grouped into **Transaction Groups**: a named set of transactions created in the CFM Persona App under **Audience Management → Transaction**. Survey workflows subscribe to one or more transaction groups, and a **Transaction Record Event** workflow fires every time a new transaction is ingested into a subscribed group.

The CFM Transaction APIs V3 expose three operations on a **single resource path** — `/api/v3/surveyTransactions` — differentiated by HTTP method:

| Operation | Method | Path | operationId |
|  --- | --- | --- | --- |
| Create transactions in a group | `POST` | `/api/v3/surveyTransactions` | `SurveyTransactionApiV3_createTransactions` |
| Fetch a transaction by ID | `GET` | `/api/v3/surveyTransactions?id=` | `SurveyTransactionApiV3_getTransaction` |
| Delete a transaction | `DELETE` | `/api/v3/surveyTransactions?id=` | `SurveyTransactionApiV3_deleteTransaction` |


This single-path, method-differentiated shape is the central design change in V3 and matches the pattern already used by Profile V3 (`/api/v3/profile`) and Survey Response V3.

> **⚠️ Conflict to resolve — resource path.** The IN-12886 Jira description documents these operations at `GET /api/v3/cfm/transactions?id=…`, `POST /api/v3/cfm/transactions`, and `DELETE /api/v3/cfm/transactions?id=…`. Both `sprinklr-v3.yaml` and all three supplied endpoint specifications use `/api/v3/surveyTransactions`. This guide follows the OpenAPI specification and the QA-verified request samples, because the specification is the artefact that generates the published reference and the SDKs. The `cfm/transactions` form must be confirmed as obsolete before publication. See [§13, Q1](#13-questions-for-the-api-owner).


There is **no bulk list operation** on `/api/v3/surveyTransactions`. `GET` returns exactly one transaction, addressed by its ID. To enumerate every transaction in a group, use the Search by Entity API — see [§5.4](#54-listing-all-transactions-in-a-group).

### 1.1 The transaction data model

The `Transaction` schema in `sprinklr-v3.yaml` is the single object used both as the request array element on `POST` and as the response payload on `GET`.

| Layer | Fields | What it holds |
|  --- | --- | --- |
| Identity | `id`, `userId`, `profileName` | The Sprinklr transaction ID, the profile identifier the transaction attaches to, and the display name |
| Grouping | `transactionGroupId`, `transactionGroupType` | The group the transaction lives in, and the group's ingestion type |
| Distribution | `distributionChannel` | The channel the survey invitation is sent on |
| Metadata | `customProperties` | Arbitrary key-value interaction metadata, mapped to Transaction Fields |
| Lifecycle | `isArchived`, `deleted`, `canEdit`, `ownerUserId`, `lastModifiedUserId`, `createdTime` | Ownership, audit, and state flags returned by the platform |
| Access control | `assetPermission` | Asset-level permission object |


> **Specification gap.** The `Transaction` schema in `sprinklr-v3.yaml` declares only `serialVersionUID`, `canEdit`, `assetPermission`, `id`, `userId`, `profileName`, `transactionGroupId`, `distributionChannel`, `transactionGroupType`, `customProperties`, and `isArchived`. It does **not** declare `ownerUserId`, `lastModifiedUserId`, `createdTime`, or `deleted`, all four of which appear in the QA-verified response samples in [§4.3](#43-example-response) and [§5.3](#53-example-response). The schema also declares no `required` array and no per-field descriptions, and it declares `customProperties` as `additionalProperties: {type: array, items: {type: string}}` (a map of string arrays) while every supplied example shows flat string values. See [§13, Q6](#13-questions-for-the-api-owner) and [§13, Q7](#13-questions-for-the-api-owner).


### 1.2 Addressing a transaction

A transaction is addressed one way only: by its **Sprinklr transaction ID**, returned in `data[].id` on create — for example `6a88545dd4bb4391d3760b0b`. There is no alternate business key. Persist the ID returned by `POST` if you intend to fetch or delete the record later.

### 1.3 Prerequisites

Three things must exist before the first API call succeeds.

1. **CFM must be enabled in your environment.** Per the Sprinklr Developer Portal: *"CFM must be enabled in your environment before you can utilize these APIs. Please contact your Success Manager for more details."*
2. **A user profile must exist for the `userId`.** A transaction attaches to a profile. If no profile exists for the identifier you send, create one first with the Create Profile API (see the Profile API V3 developer guide). Sprinklr's file-import path creates a profile automatically when one is absent; the supplied endpoint specification for `POST /surveyTransactions` states the profile is a prerequisite for the API path. See [§13, Q8](#13-questions-for-the-api-owner).
3. **A transaction group must exist, and it must be of type `API`.** Create it in the CFM Persona App under **Audience Management → Transaction → Create Transaction Group**. Per the Help Center: *"If you plan to send transactions programmatically from an external system in real time, create a Transaction Group of type API, and setup the API."* Share the group with the survey projects where the workflow will run — only shared groups are selectable inside workflows. Transactions can only be created **inside an existing group**; the API does not create groups.


## 2. Base URLs and environments

**All API calls are sent to the production endpoint:**

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

So the transaction resource in production is:

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

Replace `{env}` with your assigned environment identifier (`prod0`, `prod2`, `prod3`, `prod4`, `prod5`, `prod6`, `prod8`, `prod11`, `prod12`, `prod15`–`prod19`, `prod21`, `prod24`, `production`, `spr-uat`, `azrqa` — see [APIs | Sprinklr Developer Portal](https://dev.sprinklr.com/apis) for the authoritative list).

QA and pre-production environments use a different host shape, in which the environment is a **hostname prefix** rather than a path segment:

| Environment | Base URL | Transaction resource |
|  --- | --- | --- |
| Production | `https://api3.sprinklr.com/{env}/api/v3` | `https://api3.sprinklr.com/{env}/api/v3/surveyTransactions` |
| QA6 (internal) | `https://qa6-api2-v3.sprinklr.com/api/v3` | `https://qa6-api2-v3.sprinklr.com/api/v3/surveyTransactions` |


Do not hard-code either host. Make the base URL a single configuration value so promotion from QA to production is a config change, not a code change.

> The examples in this guide use the **QA6** form, because that is the host used verbatim in every supplied request sample. Substitute the production form before going live.


> **⚠️ Conflict to resolve — QA host name.** The supplied `DELETE` sample uses `https://qa6-api2.sprinklr.com/...` — **without** the `-v3` suffix — while the `POST` and `GET` samples and the CFM Workflow specifications all use `https://qa6-api2-v3.sprinklr.com/...`. The same sample also contains a leading space inside the quoted URL (`' https://…'`), which will produce a malformed request on some clients. Both are corrected in [§6.2](#62-example-request). See [§13, Q9](#13-questions-for-the-api-owner).


> **⚠️ Conflict to resolve — path casing and trailing slash.** The supplied `GET` specification heading reads `https://api3.sprinklr.com/{env}/api/v3/survey-transactions/{transactionId}` (kebab-case, path parameter), while its own cURL sample and the OpenAPI specification use `surveyTransactions` (camelCase, query parameter). The `POST` heading uses a trailing slash, `…/api/v3/surveyTransactions/`, which the specification does not declare. This guide uses the specification form — camelCase, no trailing slash. See [§13, Q2](#13-questions-for-the-api-owner) and [§13, Q3](#13-questions-for-the-api-owner).


## 3. Authentication and common headers

All CFM Transaction API calls are authenticated with OAuth 2.0. See [API Overview](https://dev.sprinklr.com/api-overview) for portal registration, API key and secret generation, and the Authorize flow.

| Header | Value | Purpose | Required on |
|  --- | --- | --- | --- |
| `Authorization` | `Bearer {{accessToken}}` | Authenticates the user with the server | All requests |
| `Key` | `{{apiKey}}` | Authenticates the application with the server | All requests |
| `Content-Type` | `application/json` | Declares the request body media type | `POST` |
| `Accept` | `application/json` | Declares the acceptable response type | All requests |


> **⚠️ Conflict to resolve — is `Key` mandatory?** The header tables in all three supplied specifications mark `Key` as required, but the `GET` cURL sample omits it and was nonetheless recorded as a working call. The same discrepancy appears on the CFM Workflow `/trigger` endpoint. Until this is confirmed, **send `Key` on every request** — that is the documented contract and the safe default. See [§13, Q10](#13-questions-for-the-api-owner).


> **Do not send a `Cookie` header.** The supplied `GET` cURL sample carries `Cookie: JSESSIONID=…`. That is a browser-session artefact left over from manual testing, not part of the API contract. It has been stripped from every example in this guide and must not be reproduced in client code.


> **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 in this guide is a placeholder.
**Action required.** The QA verification comment on IN-12886 (Santhosh M., 2026-08-20) contains three live-looking QA6 secrets in plaintext — two `Authorization` values and one `Key`. Those credentials must be **revoked and regenerated**, and the comment redacted, before this guide or the ticket is shared outside the immediate team. This action was raised against the CFM Workflow guide and remains open.


## 4. Create transactions

Adds one or more transactions to an existing transaction group. Each transaction represents a single interaction record for a user.

```
POST /api/v3/surveyTransactions
```

**Request schema:** `SurveyTransactionsCreateRequestDTO` — *"Create survey transactions in a transaction group."*
**Required:** `transactionGroupId`, `transactions`

### 4.1 Request body

| Parameter | Type | Required | Description |
|  --- | --- | --- | --- |
| `transactionGroupId` | String | Yes | The unique identifier of the transaction group in which the transactions are created. Create the transaction group in your CFM Persona App before calling this API. |
| `transactions` | Array of `Transaction` | Yes | The list of transactions to be processed. |
| `transactions[].userId` | String | Yes | The identifier of the user profile the transaction attaches to — typically an email address, mobile number, or unique reference ID. A profile must already exist for this value. |
| `transactions[].profileName` | String | No | The display name associated with the profile. |
| `transactions[].distributionChannel` | String | Yes | The channel used to distribute the survey invitation, for example `EMAIL`. See [§9](#9-reference-values). |
| `transactions[].customProperties` | Object | No | Free-form interaction metadata as key-value pairs, mapped to the Transaction Fields configured for the group. |
| `customProperties.orderId` | String | No | Example custom property — the order identifier for the interaction. |
| `customProperties.purchaseAmount` | String | No | Example custom property — the transaction amount. |
| `customProperties.purchaseCategory` | String | No | Example custom property — the product or service category. |


`orderId`, `purchaseAmount`, and `purchaseCategory` are **illustrative** custom properties taken from the supplied specification. They are not fixed fields. The keys your group accepts are the Transaction Fields configured for it under **Global Settings → Transaction Fields**.

> **⚠️ Conflict to resolve — `transactionGroupId` in the query string.** The supplied `POST` specification lists `transactionGroupId` in a **Query Parameter** table *and* in the request-body table. The OpenAPI specification declares **no** query parameters on `POST /surveyTransactions`, and `SurveyTransactionsCreateRequestDTO` marks `transactionGroupId` as a required body field. This guide sends it in the body only. See [§13, Q4](#13-questions-for-the-api-owner).


### 4.2 Example request

```bash
curl --location 'https://qa6-api2-v3.sprinklr.com/api/v3/surveyTransactions' \
  --header 'Authorization: Bearer {{accessToken}}' \
  --header 'Key: {{apiKey}}' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --data '{
    "transactionGroupId": "68adc96c444bb074f1fd28f7",
    "transactions": [
      {
        "userId": "jordan.rivera@example.com",
        "profileName": "Jordan Rivera",
        "distributionChannel": "EMAIL",
        "customProperties": {
          "orderId": "ORD-40219",
          "purchaseAmount": "129.00",
          "purchaseCategory": "Footwear"
        }
      }
    ]
  }'
```

> The request body in the supplied specification contains JavaScript-style `//` comments inside the JSON object, which is not valid JSON and will be rejected by a strict parser. The comments have been removed and the commented-out `customProperties` restored as real values above. The example user identifier and profile name are synthetic placeholders. See [§13, Q11](#13-questions-for-the-api-owner).


### 4.3 Example response

```json
{
  "data": [
    {
      "id": "6a88545dd4bb4391d3760b0b",
      "userId": "jordan.rivera@example.com",
      "profileName": "Jordan Rivera",
      "transactionGroupId": "68adc96c444bb074f1fd28f7",
      "distributionChannel": "EMAIL",
      "customProperties": {},
      "isArchived": false,
      "ownerUserId": 66009896,
      "createdTime": "Aug 21, 2026, 01:36:29 PM",
      "lastModifiedUserId": 66009896,
      "deleted": false,
      "canEdit": false
    }
  ],
  "errors": []
}
```

*Illustrative response, reproduced from the supplied specification with the user identifier replaced by a placeholder. No API call was executed to produce it.*

Note that `data` is an **array** on create — one element per transaction submitted, in request order — whereas `GET` returns a single object. Read `data[].id` and persist it.

> In the supplied sample, `customProperties` is returned as an empty object even though the request could carry properties, because the sample request had its properties commented out. Whether `customProperties` is echoed back on create is unconfirmed — see [§13, Q12](#13-questions-for-the-api-owner).


### 4.4 Response parameters

| Parameter | Type | Description |
|  --- | --- | --- |
| `data` | Array | The list of transactions created. |
| `data[].id` | String | The unique identifier of the created transaction. |
| `data[].userId` | String | The profile identifier the transaction is attached to. |
| `data[].profileName` | String | The display name associated with the profile. |
| `data[].transactionGroupId` | String | The identifier of the transaction group containing the transaction. |
| `data[].distributionChannel` | String | The channel used to distribute the survey invitation. |
| `data[].customProperties` | Object | The interaction metadata stored against the transaction. |
| `data[].isArchived` | Boolean | Indicates whether the transaction is archived. |
| `data[].ownerUserId` | Long | The Sprinklr user ID of the transaction owner. |
| `data[].createdTime` | String | The creation timestamp of the transaction. |
| `data[].lastModifiedUserId` | Long | The Sprinklr user ID of the last user to modify the transaction. |
| `data[].deleted` | Boolean | Indicates whether the transaction is deleted. |
| `data[].canEdit` | Boolean | Indicates whether the caller can edit the transaction. |
| `errors` | Array | The list of errors. Empty on success. |


> **Specification gap — `createdTime` format.** The samples return `createdTime` as a human-formatted string (`"Aug 21, 2026, 01:36:29 PM"`), not an ISO-8601 string or an epoch integer, and the format carries no time zone. Do not parse it with a strict ISO parser. Confirm the format and time zone before relying on it — see [§13, Q13](#13-questions-for-the-api-owner).


## 5. Fetch a transaction

Retrieves detailed information about a specific transaction using its unique transaction ID. Use it to validate and review transaction attributes, including user details, distribution channel, and status.

```
GET /api/v3/surveyTransactions?id={transactionId}
```

**Response schema:** `Transaction`

### 5.1 Request parameters

| Parameter | In | Type | Required (spec) | Description |
|  --- | --- | --- | --- | --- |
| `id` | query | String | `required: false` in the OpenAPI specification; **Required** in the supplied endpoint specification | The unique identifier of the survey transaction to retrieve. |


> **⚠️ Conflict to resolve — is `id` required?** `sprinklr-v3.yaml` declares `id` with `required: false` on both `GET` and `DELETE`, but the endpoint specification marks it Required and there is no documented behaviour for a call that omits it (no list-all semantics are declared). Treat `id` as **mandatory** in client code. See [§13, Q5](#13-questions-for-the-api-owner).


### 5.2 Example request

```bash
curl --location 'https://qa6-api2-v3.sprinklr.com/api/v3/surveyTransactions?id=6a8c446a69baf80612fb63f7' \
  --header 'Authorization: Bearer {{accessToken}}' \
  --header 'Key: {{apiKey}}' \
  --header 'Accept: application/json'
```

> The supplied sample additionally carried a `Cookie: JSESSIONID=…` header and an empty `--data ''` body on a `GET`. Both have been removed: the cookie is a session artefact, and sending a body on a `GET` causes some clients to switch the method to `POST`.


### 5.3 Example response

```json
{
  "data": {
    "id": "6a8c446a69baf80612fb63f7",
    "userId": "jordan.rivera@example.com",
    "profileName": "Jordan Rivera",
    "transactionGroupId": "68adc96c444bb074f1fd28f7",
    "distributionChannel": "EMAIL",
    "customProperties": {},
    "isArchived": false,
    "ownerUserId": 66009896,
    "createdTime": "Aug 24, 2026, 01:17:30 PM",
    "lastModifiedUserId": 66009896,
    "deleted": false,
    "canEdit": false
  },
  "errors": []
}
```

*Illustrative response, reproduced from the supplied specification with the user identifier replaced by a placeholder.*

The response parameters are identical to those in [§4.4](#44-response-parameters), except that `data` is a **single object** rather than an array.

> **⚠️ Conflict to resolve — envelope shape and response schema.** Three shapes are in play for the same resource. `POST` returns `data` as an array; `GET` returns `data` as an object. Separately, the OpenAPI specification declares the `GET` `200` response as a bare `$ref: Transaction` — **not** wrapped in the `APIResponse` envelope — while the supplied sample clearly shows the `data`/`errors` wrapper. Write your deserializer defensively and confirm which is authoritative. See [§13, Q14](#13-questions-for-the-api-owner) and [§13, Q15](#13-questions-for-the-api-owner).


### 5.4 Listing all transactions in a group

There is no bulk-list operation on `/api/v3/surveyTransactions`. To fetch **all** transactions in a transaction group:

1. Call the **Search by Entity API** with `TRANSACTION` as the `entityType` and `transactionGroupId` in the `filters` object.
2. Read the `cursor` returned in the response.
3. Pass that `cursor` to the **Search by Cursor API** to retrieve the next page, and repeat until the cursor is exhausted.


> **Specification gap.** The supplied specification names these two APIs but does not give their paths, request schemas, or the exact `filters` syntax for `transactionGroupId`. The guide cannot document the call without inventing it. See [§13, Q16](#13-questions-for-the-api-owner).


## 6. Delete a transaction

Deletes a specific transaction using its unique transaction ID.

```
DELETE /api/v3/surveyTransactions?id={transactionId}
```

### 6.1 Request parameters

| Parameter | In | Type | Required (spec) | Description |
|  --- | --- | --- | --- | --- |
| `id` | query | String | `required: false` in the OpenAPI specification; **Required** in the supplied endpoint specification | The unique identifier of the survey transaction to delete. |
| `transactionGroupId` | query | String | Undocumented | Appears in the supplied cURL sample only. Not declared in the OpenAPI specification and not present in any parameter table. |


> **⚠️ Conflict to resolve — undocumented `transactionGroupId` query parameter.** The supplied `DELETE` cURL sends `?id=…&transactionGroupId=68adc96c444bb074f1fd28f7`, but `transactionGroupId` is declared nowhere — not in the OpenAPI specification's `parameters` block and not in the specification's parameter table. It is unknown whether it is required, optional, or ignored. Until confirmed, do not depend on it. See [§13, Q17](#13-questions-for-the-api-owner).


### 6.2 Example request

```bash
curl --request DELETE 'https://qa6-api2-v3.sprinklr.com/api/v3/surveyTransactions?id=6a5e22a5155ab63b8b142d7c' \
  --header 'Authorization: Bearer {{accessToken}}' \
  --header 'Key: {{apiKey}}' \
  --header 'Accept: application/json'
```

> Corrected from the supplied sample: the leading space inside the quoted URL was removed, the host `qa6-api2.sprinklr.com` was corrected to `qa6-api2-v3.sprinklr.com`, and the undocumented `transactionGroupId` parameter was omitted pending confirmation.


### 6.3 Example response

The three sources disagree on what `DELETE` returns.

| Source | Declared response |
|  --- | --- |
| `sprinklr-v3.yaml` | `200 Success`, `application/json`, `schema: {type: string}` |
| Supplied specification — "Example Response" | `HTTP/1.1 204 (No Content)` |
| Supplied specification — parameter table | A JSON envelope: `data` (String) *"Indicates whether the survey transaction was successfully deleted"*, `errors` (Array) |


> **⚠️ Conflict to resolve — `DELETE` response.** `204 No Content` and a JSON response body are mutually exclusive; a `204` response must not carry a body. The OpenAPI specification declares `200` with a bare JSON string, which matches neither the `204` nor the `data`/`errors` envelope. The parameter table is additionally **mislabelled** "Request Parameters" while describing the response. Client code should treat any `2xx` as success and must not assume a parseable body until this is settled. See [§13, Q18](#13-questions-for-the-api-owner).


Documented on the assumption the envelope form is correct:

| Parameter | Type | Description |
|  --- | --- | --- |
| `data` | String | Indicates whether the survey transaction was successfully deleted. |
| `errors` | Array | The list of errors. Empty on success. |


> **Specification gap.** No source states whether `DELETE` is a soft delete (setting the `deleted` flag visible in the `GET` response) or a hard delete, nor whether a deleted transaction is still retrievable via `GET`. The presence of `deleted` and `isArchived` on the `Transaction` model suggests soft deletion, but that is an inference, not a documented fact. See [§13, Q19](#13-questions-for-the-api-owner).


## 7. Response format and status codes

### 7.1 The response envelope

All CFM Transaction V3 responses use the standard `APIResponse` envelope declared in `sprinklr-v3.yaml`:

| Field | Type | Description |
|  --- | --- | --- |
| `data` | Object or Array | The operation payload. An **array** of `Transaction` on `POST`; a **single** `Transaction` on `GET`; a String on `DELETE` (unconfirmed — see [§6.3](#63-example-response)). |
| `errors` | Array of `Error` | The list of errors. Empty on success. |
| `metadata` | `ResponseMetadata` | Response metadata. |


> **Specification gap.** `ResponseMetadata` is declared in `sprinklr-v3.yaml` with an empty `properties: {}` block, so no metadata field can be documented. `metadata` does not appear in any supplied response sample. Do not rely on it.


### 7.2 Status codes

Declared in `sprinklr-v3.yaml` for all three operations:

| Code | Meaning | Typical cause |
|  --- | --- | --- |
| `200` | Success | The request was processed. Inspect `errors` before assuming a complete success on `POST`. |
| `400` | Bad Request | Malformed JSON, a missing required field (`transactionGroupId` or `transactions`), a missing `userId` or `distributionChannel`, or an unrecognised `customProperties` key. |
| `401` | Unauthorized | Missing, expired, or invalid `Authorization` token. |
| `403` | Forbidden | The caller lacks permission on the transaction group, the group is not shared with the caller, or CFM is not enabled for the environment. |
| `404` | Not Found | No transaction exists for the supplied `id`, or the `transactionGroupId` does not exist. |


> **Specification gap.** `500 Internal Server Error` is **not declared** on any of the three operations, and neither is `429 Too Many Requests`. This omission is consistent across the Listening, CFM Workflow, and CFM Transaction paths in `sprinklr-v3.yaml`. Client code should still handle `5xx` defensively. See [§13, Q20](#13-questions-for-the-api-owner).


### 7.3 Error object

| Field | Type | Description |
|  --- | --- | --- |
| `errors[]` | `Error` | Error entries returned alongside or instead of `data`. |


Because `POST` accepts an array, a partial failure is possible in principle — some transactions created, others rejected. No supplied source states whether the operation is atomic per request or per element, or how a partial failure is signalled in `errors`. See [§13, Q21](#13-questions-for-the-api-owner).

## 8. Migrating from V2

### 8.1 Endpoint mapping

| V2 operation | V3 equivalent | Notes |
|  --- | --- | --- |
| Create CFM transactions | `POST /api/v3/surveyTransactions` | Body schema `SurveyTransactionsCreateRequestDTO`. `data` is returned as an array. |
| Fetch CFM transaction | `GET /api/v3/surveyTransactions?id=` | Single transaction only. No bulk fetch — see [§5.4](#54-listing-all-transactions-in-a-group). |
| Delete CFM transaction | `DELETE /api/v3/surveyTransactions?id=` | Response shape unsettled — see [§6.3](#63-example-response). |


The structural change is the same one applied across the V3 programme: **one resource path, three HTTP methods**, replacing three separately-named V2 endpoints. Routing, retry, and observability code that keys on the path must now key on the method as well.

### 8.2 What changes for a V2 client

| Area | V2 | V3 |
|  --- | --- | --- |
| Path | Distinct paths per operation | One path, `/api/v3/surveyTransactions`, method-differentiated |
| Version segment | `/api/v2/` | `/api/v3/` |
| Envelope | V2 CFM envelope | `APIResponse` — `data`, `errors`, `metadata` |
| Transaction addressing | — | `?id=` query parameter on `GET` and `DELETE` |
| Auth | OAuth 2.0 `Authorization` + `Key` | Unchanged — OAuth 2.0 `Authorization` + `Key` |
| Base URL | `https://api3.sprinklr.com/{env}/api/v2` | `https://api3.sprinklr.com/{env}/api/v3` |


> **Specification gap — V2 field-level mapping.** The V2 reference page at `https://dev.sprinklr.com/cfm-transaction-apis` returned **no retrievable body** through the connected knowledge source; only the CFM module index page describing it (*"Transactions allow you to record interaction data for customer events. Create, fetch, or delete transactions"*) was available. A field-by-field V2→V3 mapping table therefore cannot be produced without inventing the V2 request and response shapes. Supply the V2 page content, or a V2 Postman collection, and this section can be completed. See [§13, Q22](#13-questions-for-the-api-owner).


### 8.3 Migration checklist

- [ ] Confirm CFM is enabled in the target environment.
- [ ] Confirm the resource path — `/api/v3/surveyTransactions` versus the `/api/v3/cfm/transactions` form in the Jira ticket ([§13, Q1](#13-questions-for-the-api-owner)). **Blocking.**
- [ ] Move the base URL to configuration; replace the `/api/v2/` segment with `/api/v3/`.
- [ ] Replace three V2 endpoint paths with one path plus method routing.
- [ ] Confirm every transaction group used by the integration is of type **API** and shared with the target survey projects.
- [ ] Ensure a profile exists for each `userId` before creating a transaction, or create it first with the Profile API.
- [ ] Update deserializers for the `APIResponse` envelope, and handle `data` as an **array** on `POST` and an **object** on `GET`.
- [ ] Persist `data[].id` from create responses — it is the only handle for later fetch or delete.
- [ ] Send `Key` on every request, including `GET` and `DELETE`.
- [ ] Remove any `Cookie` header inherited from manual testing.
- [ ] Validate that request bodies are strict JSON — no `//` comments.
- [ ] Map your custom property keys to the Transaction Fields configured for the group.
- [ ] Handle `400`, `401`, `403`, `404` explicitly, and `5xx` defensively even though it is undeclared.
- [ ] Confirm the `DELETE` response contract before writing response-parsing logic ([§13, Q18](#13-questions-for-the-api-owner)). **Blocking.**
- [ ] Do not depend on the undocumented `transactionGroupId` parameter on `DELETE` ([§13, Q17](#13-questions-for-the-api-owner)).
- [ ] Confirm the `createdTime` string format before parsing it.
- [ ] Re-run the integration against QA6 before promoting to production.


## 9. Reference values

### 9.1 `distributionChannel`

| Value | Meaning |
|  --- | --- |
| `EMAIL` | Survey invitation distributed by email. |


> **Specification gap.** `distributionChannel` is declared as an unconstrained `type: string` in `sprinklr-v3.yaml` — there is **no enum**. `EMAIL` is the only value that appears in any supplied source. The CFM module supports Email, SMS, and WhatsApp distributions, so `SMS` and `WHATSAPP` (or `WHATSAPP_BUSINESS`) are plausible, but no source confirms the exact accepted tokens. Do not guess. See [§13, Q23](#13-questions-for-the-api-owner).


> **WhatsApp naming caution.** Where WhatsApp values do apply elsewhere in CFM, the Help Center is explicit: *"it is essential to indicate the channel name as `WHATSAPP_BUSINESS`, irrespective of whether the recipient profile is for WhatsApp or WhatsApp Business… If `WHATSAPP` is mistakenly added as a channel, it may lead to functionality problems in WhatsApp Distributions."*


### 9.2 `transactionGroupType`

| Value | Meaning |
|  --- | --- |
| `API` | Transactions are pushed programmatically from an external system. **This is the type required for the Create Transactions API.** |
| `UDC` | Transactions are ingested by the Unified Data Connector (SFTP, Amazon S3, GCS, Azure Blob). |
| `Import` | Transactions are loaded by one-time Excel or CSV file import. |


Source: Help Center — *"Workflows accept three types of transaction groups: Unified Data Connector (UDC), API, and Import."* The exact string tokens the API returns in `transactionGroupType` are not declared in `sprinklr-v3.yaml` (unconstrained `type: string`) and do not appear in any response sample. See [§13, Q24](#13-questions-for-the-api-owner).

### 9.3 `customProperties`

Free-form key-value metadata. The accepted keys are the **Transaction Fields** configured under **Global Settings → Transaction Fields** and shared globally. Values seen in the supplied specification: `orderId`, `purchaseAmount`, `purchaseCategory`. These are illustrative, not a fixed schema.

## 10. Use cases

1. **Post-purchase feedback from an e-commerce platform.** On order confirmation, `POST` a transaction carrying `orderId`, `purchaseAmount`, and `purchaseCategory` into an API-type transaction group. A Transaction Record Event workflow subscribed to that group fires and triggers a personalized survey distribution on the channel in `distributionChannel`.
2. **Refund and service-recovery surveys.** Push a transaction whenever a refund is processed, tagged with the refund reason in `customProperties`. A workflow decision node branches on the reason and routes high-value refunds to a longer service-recovery survey.
3. **Post-support-interaction surveys.** After a support ticket closes, create a transaction carrying ticket status, interaction date, and issue type. Per the Help Center, this lets surveys be *"dynamically tailored based on the nature of the support interaction, such as a technical issue or an account query."*
4. **Multiple surveys for the same customer across separate events.** A customer shops at two locations on different days. Create one transaction per visit. Because each transaction is stored independently rather than overwriting the profile, both events survive and each can drive its own targeted survey — the outcome that is impossible with profile fields alone.
5. **Loyalty programme engagement.** Create a transaction on each loyalty redemption. A workflow filters on the redemption tier in `customProperties` and sends a survey focused on the specific benefits the member received.
6. **Service-visit follow-up for field operations.** After a technician closes a visit, push a transaction with the visit type and location so the follow-up survey question set matches the work performed.
7. **Pre-flight validation of an ingestion pipeline.** After a batch `POST`, call `GET ?id=` for a sample of returned IDs and assert that `userId`, `distributionChannel`, and `customProperties` match what was sent. This is the documented purpose of the fetch endpoint — *"validate and review transaction attributes."*
8. **Correcting a mis-ingested record.** A transaction created with a wrong `userId` or an incorrect channel would otherwise trigger a survey to the wrong recipient. `DELETE` it by ID and re-create it correctly. There is no update operation on `/surveyTransactions` — correction is delete-and-recreate.
9. **Data-subject erasure requests.** When a customer requests deletion of their interaction history, enumerate their transactions via the Search by Entity API and `DELETE` each by ID. Confirm with the API owner whether this is a hard or soft delete before relying on it for a compliance workflow ([§13, Q19](#13-questions-for-the-api-owner)).


## 11. Caveats and best practices

**Create the transaction group first, and make it type `API`.** The API does not create groups. A group of type UDC or Import will not accept programmatic transaction ingestion. Share the group with the survey projects before building the workflow — only shared groups appear in the workflow group selector.

**Workflows do not backfill.** Per the Help Center: *"The workflow does not run on any transactions that were already present in the group; it only executes for transactions ingested after the workflow is activated."* Activate the workflow before you start ingesting, or the first batch will be silently ignored by the automation.

**Create the profile before the transaction.** A transaction attaches to a profile. Sequence profile creation ahead of transaction creation in your pipeline, and handle the case where profile creation fails.

**Persist the returned transaction ID.** `data[].id` is the only handle for `GET` and `DELETE`. There is no lookup by `userId`, by `orderId`, or by any other business key on this resource. Losing the ID means falling back to a Search by Entity call.

**There is no update operation.** `/api/v3/surveyTransactions` supports `POST`, `GET`, and `DELETE` only. Correcting a transaction means deleting and recreating it — and recreating may re-trigger any workflow subscribed to the group. Plan for that side effect.

**Handle the asymmetric envelope.** `data` is an array on `POST` and an object on `GET`. A single shared deserializer will fail on one of them.

**Do not assume idempotency.** No source documents an idempotency key, a deduplication window, or any uniqueness constraint on `(userId, transactionGroupId, customProperties)`. A retried `POST` will most likely create duplicate transactions — and each duplicate may fire the workflow again, sending a duplicate survey. Guard retries at the client with your own deduplication key until this is confirmed ([§13, Q25](#13-questions-for-the-api-owner)).

**Batch sensibly.** `transactions` is an array with no documented maximum length and no documented request-size limit. Until a limit is confirmed, keep batches conservative and be prepared for a `400` on oversized payloads ([§13, Q26](#13-questions-for-the-api-owner)).

**Rate limits are undocumented.** No rate-limit headers, quotas, or `429` responses are declared for these operations. Build in client-side throttling and exponential backoff regardless.

**Validate JSON strictly before sending.** The supplied create sample contained `//` comments. If you copy examples from internal documents into your client, run them through a JSON validator first.

**Keep credentials and session artefacts out of examples.** Strip `Cookie: JSESSIONID` headers, and never paste live tokens into tickets, Postman collections that are shared, or documentation.

**Time-zone caution on `createdTime`.** The returned timestamp is a formatted string with no time zone. Do not use it for ordering or SLA calculations until the format and zone are confirmed.

## 12. Quick reference

### 12.1 Operations

| Operation | Method | Path | Required input | Success payload |
|  --- | --- | --- | --- | --- |
| Create transactions | `POST` | `/api/v3/surveyTransactions` | Body: `transactionGroupId`, `transactions[]` (each with `userId`, `distributionChannel`) | `200` — `data` = array of `Transaction` |
| Fetch transaction | `GET` | `/api/v3/surveyTransactions?id=` | Query: `id` | `200` — `data` = single `Transaction` |
| Delete transaction | `DELETE` | `/api/v3/surveyTransactions?id=` | Query: `id` | `200` — response shape unsettled ([§6.3](#63-example-response)) |


### 12.2 Related CFM V3 endpoints not covered by this guide

| Endpoint | Method | Purpose |
|  --- | --- | --- |
| `/api/v3/cfmWorkflow/trigger` | `POST` | Trigger a CFM workflow for an existing profile. See the CFM Workflow API V3 developer guide. |
| `/api/v3/cfmWorkflow/triggerWithNewProfile` | `POST` | Create or update a profile and trigger a CFM workflow. See the CFM Workflow API V3 developer guide. |
| `/api/v3/surveyDistribution/transactions/link` | `POST` | Create a personalized survey transaction link (`CFMSurveyDistributionApiV3_createTransactionLink`, body `SurveyTransactionRequestDTO`). |
| Survey Response V3 | various | Fetch by ID, ingest, update custom fields, delete, and search survey responses. |


> The `SurveyTransactionRequestDTO` schema backing the personalized-link endpoint is declared in `sprinklr-v3.yaml` with an empty `properties: {}` block and only the description *"Describes the request for creating transaction."* Its request body cannot be documented from the specification.


### 12.3 Required headers

```
Authorization: Bearer {{accessToken}}
Key: {{apiKey}}
Content-Type: application/json      # POST only
Accept: application/json
```

## 13. Questions for the API owner

Ownership drawn from IN-12886: assignee **Aman Joshi**, reporter **Ruchika Grover**, reviewer **Prateek Agrawal**, QA **Santhosh M.**

Note that IN-12886's QA comment records that *"CFM Transaction APIs have already been covered in another Postman collection,"* so — unlike the CFM Workflow endpoints — **no verified cURL samples were pasted into the ticket** for cross-checking. That collection should be attached to the ticket; several questions below would be answered by it.

| # | Question | Why it blocks | Suggested owner |
|  --- | --- | --- | --- |
| 1 | Is the resource path `/api/v3/surveyTransactions` (spec + endpoint specs) or `/api/v3/cfm/transactions` (Jira description)? | **Blocking.** Every example, SDK method, and client integration depends on it. | Aman Joshi |
| 2 | Is the path camelCase `surveyTransactions` or kebab-case `survey-transactions`? | **Blocking.** The Fetch specification heading uses kebab-case; everything else uses camelCase. | Aman Joshi |
| 3 | Is `id` a query parameter (`?id=`) or a path parameter (`/{id}`)? Does a trailing slash matter on `POST`? | **Blocking.** Fetch and Delete specification headings use `/{id}`; their own cURLs and the spec use `?id=`. | Aman Joshi |
| 4 | Does `POST` accept `transactionGroupId` as a query parameter as well as a body field? | Determines whether the query-parameter table in the Create specification should be published or removed. | Aman Joshi |
| 5 | Is `id` genuinely optional (`required: false`) on `GET` and `DELETE`? If so, what is the behaviour when it is omitted? | Determines client validation and whether an accidental list-all or mass-delete is possible. | Aman Joshi |
| 6 | Should `Transaction` in `sprinklr-v3.yaml` declare `ownerUserId`, `lastModifiedUserId`, `createdTime`, and `deleted`? They appear in every response sample but not in the schema. | Generated SDKs and the published reference will silently drop these four fields. | Prateek Agrawal |
| 7 | Is `customProperties` a map of strings (as in every example) or a map of string arrays (as declared in the schema)? | Wrong typing breaks deserialization. | Prateek Agrawal |
| 8 | Must a profile exist before creating a transaction, or does the API create one automatically as the file-import path does? | Determines whether clients need a profile-create pre-step. | Ruchika Grover |
| 9 | Is the QA6 host `qa6-api2-v3.sprinklr.com` for all three methods? The Delete sample uses `qa6-api2.sprinklr.com`. | A wrong host produces a confusing DNS or routing failure in testing. | Santhosh M. |
| 10 | Is the `Key` header mandatory on `GET` and `DELETE`? The verified Fetch cURL omits it. | Same open question as CFM Workflow `/trigger`. Affects auth handling. | Aman Joshi |
| 11 | Can the Create example in the source specification be corrected to valid JSON (removing the `//` comments)? | Copy-paste of the current sample fails against a strict parser. | Ruchika Grover |
| 12 | Are `customProperties` echoed back in the create response? The sample returns `{}`. | Determines whether clients can trust the create response as confirmation of stored metadata. | Aman Joshi |
| 13 | What is the exact format and time zone of `createdTime` (`"Aug 21, 2026, 01:36:29 PM"`)? Can it be ISO-8601 or epoch instead? | The current format is unparseable by standard date libraries and carries no zone. | Prateek Agrawal |
| 14 | Why is `data` an array on `POST` but an object on `GET`? Is this intentional? | Forces two deserializers for one resource. | Prateek Agrawal |
| 15 | Does `GET` return a bare `Transaction` (as the spec declares) or an `APIResponse` envelope (as the sample shows)? | **Blocking.** The two are incompatible. | Aman Joshi |
| 16 | What are the paths and request schemas for the Search by Entity and Search by Cursor APIs, and the exact `filters` syntax for `transactionGroupId`? | The only documented way to list a group's transactions cannot be written up without them. | Aman Joshi |
| 17 | Is `transactionGroupId` a valid parameter on `DELETE`? Required, optional, or ignored? | It appears in the sample cURL and nowhere else. | Aman Joshi |
| 18 | What does `DELETE` actually return — `204 No Content`, `200` with a bare string, or `200` with a `data`/`errors` envelope? | **Blocking.** All three are documented and they are mutually exclusive. | Aman Joshi |
| 19 | Is `DELETE` a hard delete or a soft delete? Is a deleted transaction still retrievable via `GET` with `deleted: true`? | Determines whether the endpoint is usable for data-subject erasure. | Ruchika Grover |
| 20 | Should `500` and `429` be declared on these operations? | Undeclared error responses lead to unhandled paths in generated clients. | Prateek Agrawal |
| 21 | Is `POST` atomic across the `transactions` array, or can it partially succeed? How is a partial failure signalled in `errors`? | Determines retry safety on batch ingestion. | Aman Joshi |
| 22 | Can the V2 `cfm-transaction-apis` page content or a V2 Postman collection be supplied? | The V2 page body is not retrievable, so [§8.2](#82-what-changes-for-a-v2-client) has no field-level mapping. | Ruchika Grover |
| 23 | What is the complete accepted set of `distributionChannel` values? Should it be an enum in the spec? | Only `EMAIL` is evidenced. Guessing `SMS`/`WHATSAPP` risks silent failures. | Aman Joshi |
| 24 | What string tokens does `transactionGroupType` return, and should it be an enum? | Not evidenced in any sample. | Prateek Agrawal |
| 25 | Is `POST` idempotent? Is there a deduplication key or window, or a uniqueness constraint? | A retried create may duplicate transactions and fire duplicate surveys. | Aman Joshi |
| 26 | What is the maximum length of the `transactions` array and the maximum request body size? Are there rate limits or quotas? | Needed to size batches and build backoff. | Aman Joshi |
| 27 | Can the plaintext QA6 credentials in the IN-12886 QA comment be revoked, regenerated, and redacted? Can the separate Transaction Postman collection be attached to the ticket? | **Security action, still open.** Live-looking tokens are exposed in the ticket. | Santhosh M. |


*All examples in this guide are illustrative and were assembled from the supplied endpoint specifications, `sprinklr-v3.yaml`, Jira IN-12886, and the Sprinklr Developer Portal and Help Center. All credentials are placeholders and all user identifiers are synthetic. No API call was executed and no specification linting or validation was run to produce this document. Items marked as conflicts or specification gaps must be resolved by the API owner before publication.*