Quality API: CSAT response IDs not matching interaction IDs

I’m trying to map CSAT survey responses back to specific voice interactions using the Node.js SDK. The goal is to pull a list of interactions, filter for those with a CSAT score, and then enrich the data with the actual response text.

The issue is that the interactionId in the Quality API response doesn’t seem to match the id from the Conversations API. I’m using the /api/v2/quality/evaluations endpoint first to get the evaluation ID, then calling /api/v2/quality/evaluations/{id} to get the details. The response has a surveyResponse object, but the interactionId there is a different format than what I get from /api/v2/conversations/voice/{id}.

Here’s the relevant snippet:

const evaluation = await api.qualityApi.getEvaluation(id);
const surveyResp = evaluation.body.surveyResponse;
console.log('Eval Interaction ID:', surveyResp.interactionId);
// Output: Eval Interaction ID: 1234567890
// Conversations API ID: abc-def-123456

I’ve checked the docs and it says the interaction ID should be consistent across APIs. Am I missing a step to resolve the ID? Or is there a separate endpoint to map these?

I’ve also tried using the Analytics API to pull CSAT metrics, but that only gives me aggregated data, not the individual responses. I need the text of the response for sentiment analysis later.

Any ideas on how to bridge this gap?

You’re hitting a common mismatch. The Quality API doesn’t use the same ID namespace as the Conversations API by default. When you pull from /api/v2/quality/evaluations, the interactionId field usually points to the internal evaluation reference, not the public conversation ID you see in the UI or get from the Conversations API.

To map them correctly, you need to look at the interaction object within the evaluation response, specifically the id field if it’s a voice interaction, but more reliably, you should query the Interaction API using the interactionId from the evaluation as a filter.

Here is how I structure the lookup in Node.js to bridge that gap:

const platformClient = require('genesyscloud');

// 1. Get the evaluation first
const qualityApi = new platformClient.QualityApi();
const evalResponse = await qualityApi.postQualityEvaluationsQuery({
 body: {
 query: {
 filters: [
 { field: 'evaluatedBy.id', operator: 'eq', value: 'agent-id-here' }
 ]
 }
 }
});

// 2. Extract the internal interaction ID
const internalInteractionId = evalResponse.entities[0].interactionId;

// 3. Use the Conversations API to find the matching conversation
const conversationsApi = new platformClient.ConversationsApi();
const convResponse = await conversationsApi.postConversationsSearchGet({
 body: {
 query: {
 filters: [
 { field: 'id', operator: 'eq', value: internalInteractionId } 
 // Note: Sometimes you need to use 'genesysId' or check the interaction type
 ]
 }
 }
});

If the direct ID match fails, check the interactionType. For voice, the IDs usually align, but for digital channels, the interactionId in Quality might be a sub-interaction ID. You’ll need to correlate via the externalId or timestamp ranges if the direct lookup returns empty. Also, ensure your OAuth token has quality:evaluations:view and conversations:view scopes. Missing scopes can cause silent failures in the search query.

The docs for GetQualityEvaluations explicitly state that interactionId is opaque. It’s not a conversation ID. You can’t just join on it.

If you’re using the .NET SDK, stop trying to map IDs manually. It’s a waste of cycles. Use the expand parameter to pull the interaction details directly in the evaluation call.

var apiInstance = new QualityApi();
var opts = new GetQualityEvaluationsOptions
{
 Expand = new List<string> { "interaction" }
};

var result = await apiInstance.GetQualityEvaluations(opts);
foreach (var eval in result.Entities)
{
 if (eval.Interaction != null)
 {
 Console.WriteLine($"Conversation ID: {eval.Interaction.Id}");
 Console.WriteLine($"Score: {eval.Score}");
 }
}

This returns the actual conversation object nested inside. No extra API calls. The previous suggestion about parsing JSON is fine if you’re using raw HTTP, but the SDK handles the deserialization. Just make sure you have the view:quality scope. Otherwise you’ll get a 403.