Designing Cross-Interaction Survey Stitching for Measuring Cumulative Customer Experience
What This Guide Covers
This guide details the architectural pattern for correlating disparate customer interactions (voice, digital, email) with post-interaction survey responses to calculate a unified Customer Effort Score (CES) or Net Promoter Score (NPS) across a customer journey. The end result is a normalized dataset where every survey response is linked to the specific interaction transcript, agent metadata, and historical context, enabling accurate root-cause analysis for negative sentiment.
Prerequisites, Roles & Licensing
- Licensing:
- Genesys Cloud CX: CX 2 or CX 3 license tier (required for Architect Survey blocks and Interaction Analytics).
- Add-on: Speech Analytics (if analyzing voice transcripts) or Digital Analytics (if analyzing chat/email text).
- Permissions:
Interaction > Survey > ViewandInteraction > Survey > EditArchitect > Flow > ViewandArchitect > Flow > EditReporting > Report > ViewandReporting > Report > Edit
- External Dependencies:
- A downstream data lake or BI tool (e.g., Snowflake, Tableau, Power BI) capable of joining large datasets.
- Genesys Cloud Interaction Data Export (IDE) or custom API polling infrastructure.
The Implementation Deep-Dive
1. Establishing the Correlation Key Strategy
The fundamental challenge in cross-interaction stitching is that Genesys Cloud generates unique identifiers for different entities: interaction.id, call.id, chat.id, and survey.response.id. These IDs are not inherently linked in a parent-child relationship within the UI. To stitch them, you must propagate a stable, persistent identifier through every stage of the interaction lifecycle.
We use the Customer-ID (typically a CRM Account Number or External User ID) as the primary correlation key. If a Customer-ID is unavailable, we fall back to Phone-Number or Email-Address, normalized to a canonical format.
The Trap: Relying solely on interaction.id for stitching.
interaction.id is unique to a single session. If a customer calls, then chats, then emails, they generate three distinct interaction.id values. If you only stitch by interaction.id, you create siloed metrics. You cannot measure the “Cumulative Experience” because you cannot see that the same customer had three negative interactions in 24 hours. You must pivot the data model from “Interaction-Centric” to “Customer-Centric.”
Architect Configuration: Propagating the Key
In Genesys Cloud Architect, you must capture the Customer-ID as early as possible and pass it explicitly to the Survey block.
- Capture: Use a
Get Customer Infoblock or an API block to retrieve the Customer-ID from your CRM or a local lookup table. Store this in a flow variable namedcust_id. - Sanitize: Ensure
cust_idcontains no special characters that might break URL encoding or SQL injection patterns if passed to external systems. - Inject into Survey: When configuring the
Surveyblock in Architect:- Go to the Survey configuration tab.
- Under Custom Fields, map the variable
cust_idto a custom field namedx_cust_id. - Critical: Do not rely on the default
Caller IDfield. Caller IDs are often spoofed, masked, or inconsistent (e.g.,+15551234567vs(555) 123-4567).
Architect Expression Example:
If you are constructing the survey URL dynamically for a digital channel or an IVR SMS link, you must append the correlation key:
// Architect Expression for constructing a survey URL with tracking
"https://survey.genesys.cloud/respond?surveyId=" + surveyId + "&ref=" + cust_id
For voice channels, the x_cust_id custom field is embedded in the survey response payload sent to Genesys.
2. Configuring Survey Response Payloads via API
To stitch data outside of the Genesys UI, you must access the raw survey responses via the Genesys Cloud REST API. The default reporting tables often aggregate data too heavily for granular stitching.
We use the Survey Response API to retrieve individual responses. The key is to ensure the response object contains the x_cust_id custom field we injected in Architect.
HTTP Method: GET
Endpoint: /api/v2/surveys/responses
Query Parameters:
dateFrom: ISO 8601 timestamp (e.g.,2023-10-01T00:00:00.000Z)dateTo: ISO 8601 timestampexpand:customFields(This is mandatory. Without this, the API returns only the standard fields, and yourx_cust_idwill be missing.)
Sample Response Payload:
{
"id": "response-uuid-123",
"surveyId": "survey-uuid-456",
"surveyName": "Post-Call CSAT",
"responseDate": "2023-10-05T14:30:00.000Z",
"interactionId": "interaction-uuid-789",
"customFields": [
{
"name": "x_cust_id",
"value": "CRM-ACC-998877"
}
],
"answers": [
{
"questionId": "q1",
"questionName": "Overall Satisfaction",
"answerValue": "2"
}
]
}
The Trap: Ignoring the interactionId in the survey response.
The interactionId in the survey response is the bridge to the actual conversation. Many architects assume the survey response contains the transcript or agent details. It does not. The survey response is a lightweight record. You must join survey.response.interactionId to interaction.id in your data lake to retrieve the media, transcripts, and agent metadata.
3. Building the Interaction Data Join
Once you have the survey responses with x_cust_id and interactionId, you must pull the corresponding interaction data. This requires using the Interaction Data Export (IDE) or polling the Interaction API for detailed metadata.
Step 3a: Retrieving Interaction Metadata
Use the Interaction API to fetch the details of the specific interaction linked to the survey.
HTTP Method: GET
Endpoint: /api/v2/interactions/{interactionId}
Required OAuth Scope: interaction:view
Sample Interaction Payload (Relevant Fields):
{
"id": "interaction-uuid-789",
"type": "voice",
"initiationDirection": "inbound",
"customer": {
"id": "customer-uuid-abc",
"phone": "+15551234567",
"email": "user@example.com"
},
"routing": {
"queueId": "queue-uuid-xyz",
"agentId": "agent-uuid-def",
"wrapUpCode": "Resolved"
},
"media": [
{
"id": "media-uuid-123",
"type": "voice",
"start": "2023-10-05T14:00:00.000Z",
"end": "2023-10-05T14:05:00.000Z"
}
]
}
Step 3b: Normalizing the Customer Key
In your data transformation layer (e.g., dbt, Spark, or Python Pandas), you must normalize the customer identifier across both datasets.
- Extract
x_cust_idfrom the Survey ResponsecustomFieldsarray. - Extract
customer.phoneorcustomer.emailfrom the Interaction payload ifx_cust_idwas not captured (fallback scenario). - Create a unified
customer_keycolumn.
The Trap: Mismatched Data Types.
The x_cust_id in the survey response is a string. The customer.id in the interaction API might be a UUID or a different string format. If your CRM passes 12345 as an integer in one system and "12345" as a string in another, joins will fail. Always cast all correlation keys to VARCHAR or STRING in the ETL process before joining.
4. Calculating Cumulative Metrics
With the joined dataset, you can now calculate cumulative metrics. The standard approach is to define a “Customer Journey Window” (e.g., 7 days, 30 days).
Metric Definition: Cumulative CES
Customer Effort Score (CES) is typically a 1-5 scale. A cumulative CES should not be a simple average. A single “5” (Very High Effort) after two "1"s (Very Low Effort) does not cancel out the frustration. We use a Weighted Recency Model.
Formula:
$$ Cumulative CES = \frac{\sum (Score_i \times Weight_i)}{\sum Weight_i} $$
Where $Weight_i = \frac{1}{TimeDifference_i + 1}$
This formula gives higher weight to more recent interactions. An interaction from today weighs more than an interaction from last week.
Implementation in SQL/BI Tool
Assuming you have a table survey_interaction_joined with columns: customer_key, survey_score, interaction_timestamp.
WITH interaction_weights AS (
SELECT
customer_key,
survey_score,
interaction_timestamp,
-- Calculate days since the reference date (e.g., today)
DATEDIFF(day, interaction_timestamp, CURRENT_DATE) as days_ago,
-- Apply recency weight: 1 / (days_ago + 1)
1.0 / (DATEDIFF(day, interaction_timestamp, CURRENT_DATE) + 1) as recency_weight
FROM
survey_interaction_joined
WHERE
interaction_timestamp >= DATEADD(day, -30, CURRENT_DATE) -- 30-day window
)
SELECT
customer_key,
-- Weighted Average Calculation
SUM(survey_score * recency_weight) / SUM(recency_weight) as cumulative_ces,
COUNT(*) as total_interactions_in_window
FROM
interaction_weights
GROUP BY
customer_key
HAVING
total_interactions_in_window > 1 -- Only show customers with multiple interactions
The Trap: Dividing by Zero or Null Scores.
If a customer submits a survey but skips the scoring question, survey_score is NULL. In SQL, NULL * Weight is NULL. You must filter out NULL scores before aggregation, or use COALESCE(survey_score, 0) if your business logic dictates that non-responses are treated as neutral (rarely recommended).
Validation, Edge Cases & Troubleshooting
Edge Case 1: Multi-Channel Interaction Mismatch
The Failure Condition:
A customer calls (Voice), then follows up with a Chat. The Voice interaction has x_cust_id = “CRM-999”. The Chat interaction has x_cust_id = NULL because the chat widget did not capture the CRM ID before the survey was triggered.
The Root Cause:
The Chat Architect flow did not execute the “Get Customer Info” block before the Survey block, or the cookie/IDP session expired.
The Solution:
Implement a “Fallback Stitching” logic in your ETL.
- If
x_cust_idis NULL in the survey response, look at theinteraction.customer.emailorinteraction.customer.phone. - Join the Survey Response to the Interaction via
interactionId. - Use the Interaction’s customer email/phone to join to the CRM master table to resolve the
customer_key. - Warning: This is less reliable due to email typos or shared family phone numbers. Prioritize the explicit
x_cust_idcapture in Architect.
Edge Case 2: Survey Response Lag and Data Freshness
The Failure Condition:
You run a daily report on Cumulative CES. The report shows a customer with a low CES, but the customer actually submitted a high CES survey 2 hours ago. The report is stale.
The Root Cause:
Survey responses are not always immediately available in the reporting tables. There is a processing latency (typically 15-30 minutes) between survey submission and API availability. Furthermore, if you are using Interaction Data Export (IDE), batches may be delayed.
The Solution:
- API Polling: For real-time dashboards, poll the
/api/v2/surveys/responsesendpoint every 5-10 minutes for the last 24 hours. - Timestamp Filtering: Always filter survey responses by
responseDatein the last N hours, not byinteractionDate. A customer might interact at 10:00 AM but submit the survey at 2:00 PM. The “Cumulative Experience” should reflect the survey time, not the call time, for recency weighting. - Idempotency: Ensure your ETL pipeline handles duplicate records. If you poll frequently, you may fetch the same survey response twice. Use
survey.response.idas the unique key to upsert records.
Edge Case 3: Agent Wrap-Up Code Mismatch
The Failure Condition:
You attempt to correlate negative surveys with specific agent behaviors. You find that agents with high wrap-up rates still receive poor survey scores. The correlation is weak.
The Root Cause:
The wrapUpCode in the interaction payload is set by the agent at the end of the call. It is subjective. The survey score is set by the customer. They are not causally linked. However, if you are trying to stitch survey data to Speech Analytics sentiment scores, you must ensure the interactionId matches exactly.
The Solution:
Do not rely on Wrap-Up Codes for stitching. Use the interactionId to join Survey Data to Speech Analytics Transcripts.
- Fetch Speech Analytics results via
/api/v2/analytics/conversations/details/query. - Filter by
interactionId=survey.response.interactionId. - Compare
sentiment.scorefrom Speech Analytics withsurvey_score. - This allows you to identify “Hidden Negatives”: Calls where the agent marked “Resolved” and Speech Analytics shows Neutral Sentiment, but the Customer gave a 1/5 score. These are your highest-priority coaching opportunities.