Customizable CSAT API

Customizable CSAT API

In this article I deep dive into the new API endpoints for the customizable customer satisfaction feature Zendesk recently announced. I go over the available API data and turn it into something real by making an app that shows survey results for your agents.

Last month Zendesk released their new Customizable CSAT feature. This replaces the old thumbs up/down customer satisfaction score with a more powerful version that allows for different rating scales (2, 3 or 5 choices) and you can choose emoji, text or numbers to display the available options.

When enabled this new scoring method will replace the old voting system in both your emails and messaging conversations, giving customers more options to give their feedback.

Overview of the new Customizable CSAT for Zendesk
The new Customisable CSAT for Zendesk has arrived, finally allowing you to change your rating scale, choose emoji, numbers or labels, and customize your follow-up questions. This article contains an initial overview of the new feature, and shows how it works with existing API integrations.

There is however a limitation. For now, the Agent Workspace does not reflect these new options, and will still show the old rating for your agents. Similarly Zendesk Explore also shows the old rating system. The system does this by converting the new scoring system back to a binary good/bad.
For example if you give the option a scale of five options, the first three will be counted as negative, and 4/5 are counted as positive feedback.

For now, there's no native way to show the new rating for your agents, but there is however an API available that makes the new scoring available!

Customizable CSAT API

The old CSAT ratings are available as data under the Ticket API endpoints. The new survey responses are moved under the Help Center APIs and are available via an API call to GET /api/v2/guide/{locale}/survey_responses/{id}

Survey Responses
Developer documentation for products at Zendesk

Getting that id however is not that easy to do. It doesn't respond to a ticket id, but will require a custom id for your survey.

Retrieving a survey response ID

Survey ID's are found via the ticket audit log and requires you to know the ticket.id of the ticket you want to fetch surveys for:

GET /api/v2/tickets/${ticket}/audits.json

Calling this API will return a long audit log of everything that happened on the ticket. You'll want to look for SurveyResponseSubmitted and store the survey_response_id

{
    "audits": [
          {
            "ticket_id": 4394,
            "events": [
                {
                    "id": 21625784786322,
                    "type": "Change",
                    "value": "Perfect",
                    "field_name": "satisfaction_comment",
                    "previous_value": null
                },
                {
                    "id": 21625784786450,
                    "type": "Change",
                    "value": "good",
                    "field_name": "satisfaction_score",
                    "previous_value": "offered"
                },
                {
                    "id": 21625784788370,
                    "type": "SurveyResponseSubmitted",
                    "survey_type": "CustomerSatisfaction",
                    "survey_response_id": "01J8QYS0BBHBVZN2EJ0SBF3K17",
                    "assigned_user_id": 362397585840,
                    "assigned_group_id": 360001292600
                }
            ]
        }
    ]
}

Note that you can also use this audit log to check if a survey was submitted by looking for SurveyOffered so you don't make API calls for surveys that aren't requested

{
    "audits": [
        {
            "ticket_id": 4394,
            "events": [
                {
                    "id": 21625552921234,
                    "type": "SurveyOffered",
                    "survey_type": "CustomerSatisfaction",
                    "survey_id": "01HFW9WGNYRRZ7MACXRAB0AZN9",
                    "assigned_user_id": 362397585840,
                    "assigned_group_id": 360001292600
                }
            ]
        }

    ]
}

Getting the Survey response

Now that we've got our survey_response_id we can use it to retrieve the survey itself.

The API endpoint for this is GET /api/v2/guide/${locale}/survey_responses/${survey_response_id} . Note that we need a locale for out API url. Since CSAT surveys support dynamic content, we can show the questions and scoring options in multiple languages to our end-users. If we want the API data returned to mirror the actual options the customer saw, make sure you set the locale to that of the customer. For this article, I'll use en-us since that's what my Zendesk instance uses for all customers.

If we retrieve the survey response for our sample ticket above, we'll get the following return:

{
    "survey_response": {
        "id": "01J8QYS0BBHBVZN2EJ0SBF3K17",
        "expires_at": "2024-10-24T20:01:21.771Z",
        "survey_id": "01HFW9WGNYRRZ7MACXRAB0AZN9",
        "survey_version": 14,
        "answers": [
            {
                "type": "rating_scale",
                "rating": 3,
                "rating_category": "good",
                "question": {
                    "type": "rating_scale_emoji",
                    "id": "01HFW9QMFZMC05R3R7NG02QX86",
                    "alias": "customer satisfaction rating",
                    "sub_type": "customer_satisfaction",
                    "headline": {
                        "type": "static",
                        "value": "How would you rate the support you received?"
                    },
                    "options": [
                        {
                            "rating": 1,
                            "emoji": {
                                "type": "static",
                                "value": "😞"
                            },
                            "label": {
                                "type": "static",
                                "value": "Ugly"
                            }
                        },
                        {
                            "rating": 2,
                            "emoji": {
                                "type": "static",
                                "value": "😐"
                            },
                            "label": {
                                "type": "static",
                                "value": "Bad"
                            }
                        },
                        {
                            "rating": 3,
                            "emoji": {
                                "type": "static",
                                "value": "πŸ˜€"
                            },
                            "label": {
                                "type": "static",
                                "value": "Good"
                            }
                        }
                    ]
                },
                "created_at": "2024-09-26T20:19:43.543Z",
                "updated_at": "2024-09-26T20:19:43.543Z"
            },
            {
                "type": "open_ended",
                "value": "Perfect",
                "question": {
                    "type": "open_ended",
                    "id": "01HFW9QMFZD8HZNZJ6JX7ZGQ5X",
                    "alias": "comment",
                    "headline": {
                        "type": "static",
                        "value": "Share your thoughts on the support you received"
                    }
                },
                "created_at": "2024-09-26T20:19:43.543Z",
                "updated_at": "2024-09-26T20:19:43.543Z"
            }
        ]
    }
}

Useful here is that the returned data contains not only the feedback given, but also contains the actual survey options available at that time. Which means that if you decide to change your rating from 1-5 back to 1-3 in the future, this survey will still reflect the available choices at that time.

Reading the survey

Looking at the payload above there's a few elements to highlight.

Rating Scale

The first element of our API data is a rating_scale object that contains

  • How many choices where presented to the customer (2, 3 or 5)
  • The rating_category which can be good or bad
  • The actual rating given by the customer, in this case 3.
{
  "type": "rating_scale",
  "rating": 3,
  "rating_category": "good",
  "question": {}
}

Each rating survey also has the option to have an open ended question that allows for comments from the customer. This can be found as open_ended in the payload, and contains a value element with the customers comments.

{
  "type": "open_ended",
  "value": "Perfect",
  "question": {}
}

If the customer gave bad feedback, we also get a closed_ended question with a list of predefined dropdown options. The customers' answer can be found under selections[0].option_id and you need to look up the actual label of that choice in the question.options[].label.value

{
  "type": "closed_ended",
  "selections": [
    {
      "type": "predefined",
      "option_id": "01HFW9QMFZ9Z2RYR0H51K37NBK"
    }
  ],
  "question": {
    "type": "closed_ended",
    "id": "01HFW9QMFZXZBKCTCB8TNWNV6C",
    "alias": "reason",
    "headline": {
      "type": "static",
      "value": "Select a reason for your experience"
    },
    "options": [
      {
        "id": "01HFW9QMFZ9Z2RYR0H51K37NBK",
         "label": {
          "type": "static",
          "value": "The issue took too long to resolve"
        }
      },
      {
       "id": "01HFW9QMFZDW9VXZ0JCGNKY903",
       "label": {
          "type": "static",
          "value": "The information provided was not clear or helpful"
        }
      },
      {
        "id": "01HFWCAJ3ZVVZDMH6GNYRWSWN5",
        "label": {
          "type": "static",
          "value": "The issue was not resolved"
        }
      }
    ]
  }
}

Turning this into something useful

Having the API to get the data is one thing, but actually doing something with that data is the actual point of having the API.

So I decided to build a small Zendesk utility that displays the current CSAT rating in the sidebar next to tickets.

πŸ’‘
If you're interesting in testing this app, I've got it available as preview app on the Zendesk Marketplace. It's not for sale but if you want to test it out and are an internal Note Plus subscriber, let me know!

How does it work?

The app first gets the current ticket id via:

// get current ticket id
  var ticket = await client.get('ticket.id');
  ticket = ticket['ticket.id'];

It then gets the current audit log for that ticket

// get current ticket audit log
  var audit = await client.request(`/api/v2/tickets/${ticket}/audits.json`);
  audit = audit['audits'];

We then look for a survey_response_id and survey_type to check if a survey was send out, and if so what the id is

// find survey_response_id and survey_offered_type in audit log
    var survey_response_id;
    var survey_offered_type;
    var survey_offered_date;
    for (var i = 0; i < audit.length; i++) {
        for (var j = 0; j < audit[i].events.length; j++) {
            if (audit[i].events[j].type == 'SurveyResponseSubmitted') {
                survey_response_id = audit[i].events[j].survey_response_id;
            } else if (audit[i].events[j].type == 'SurveyOffered') {
                survey_offered_type = audit[i].events[j].type;
                survey_offered_date = audit[i].created_at;
            }
            if (survey_response_id && survey_offered_type) break;
        }
        if (survey_response_id && survey_offered_type) break;
    }

if there is a submitted survey we then retrieve the responses via API and render a nice preview of the survey:

// get survey response
var survey_response = await client.request(`/api/v2/guide/en-us/survey_responses/${survey_response_id}`);
survey_response = survey_response;

And we use that survey response to render a nice preview of the responses in the sidebar of our app by going over the answers in the response and rendering them out as html.

var surveyHTML = generateSurveyHTML(survey_response);
$('body').append(surveyHTML);

function generateSurveyHTML(response) {
	var html = $('<div>');

	// Handle Rating Scale Section
	var ratingScale = $('<div>');
	var ratingQuestion = response.survey_response.answers.find(answer => answer.type === 'rating_scale');
	
	if (ratingQuestion) {
		ratingScale.append($('<h3>').text(ratingQuestion.question.headline.value);		
		var ratingList = $('<ul>');
		
		ratingQuestion.question.options.forEach(function(option) {
			var listItem = $('<li>');
			var label = $('<label>');
			
			var radio = $('<input>', {
				type: 'radio',
				name: 'rating',
				value: option.rating,
				disabled: true,
				checked: ratingQuestion.rating === option.rating
			});
			
			var emojiLabel = $('<span>').text(option.emoji.value + ' ' + option.label.value);
			
			label.append(radio);
			label.append(emojiLabel);
			
			listItem.append(label);
			ratingList.append(listItem);
		});
		
		ratingScale.append(ratingList);
		
		// Add the date when the rating was created
		var ratingDate = new Date(ratingQuestion.created_at).toLocaleString();
		ratingScale.append($('<p>').text('Rating submitted on: ' + ratingDate));

		html.append(ratingScale);
	}

	// Handle Closed-Ended Question Section (Selection)
	var closedEndedQuestion = response.survey_response.answers.find(answer => answer.type === 'closed_ended');
	
	if (closedEndedQuestion) {
		var closedEnded = $('<div>');	
		closedEnded.append($('<h3>').text(closedEndedQuestion.question.headline.value));	
		var selectedOption = closedEndedQuestion.selections[0].option_id;
		var selectedLabel = closedEndedQuestion.question.options.find(option => option.id === selectedOption).label.value;	
		closedEnded.append($('<p>').text(selectedLabel));
		html.append(closedEnded);
	}

	// Handle Open-Ended Question Section
	var openEndedQuestion = response.survey_response.answers.find(answer => answer.type === 'open_ended');
	
	if (openEndedQuestion) {
		var openEnded = $('<div>');
		openEnded.append($('<h3>').text(openEndedQuestion.question.headline.value));
		openEnded.append($('<p>').text(openEndedQuestion.value));
		html.append(openEnded);
	}

	return html;
}   

Conclusion

The new Customizable CSAT survey API is quite easy to use and allows for insights that currently aren't available via native UI features in Zendesk. I do hope that sooner rather than later the Agent Workspace will natively reflect the new survey responses, making apps like mine unnecessary.

As for the API itself, I'd love for a way to retrieve surveys for a given ticket or user more directly than going via the Audit Log api for a given ticket. There's also currently no POST or PUT endpoint so you can't create integrations that allow customers to give feedback. You have to go over the native Messaging or Email triggers.