How we enforce case tagging using Intercom’s API and Webhooks

Gerald Onyango
Jan 27 · 6 min read
Photo by rawpixel on Unsplash

On our path to being a more data-driven company, we at Netlify want to ensure that we have good metrics from our helpdesk. This way, we can keep track of what people are asking, why they are contacting us, and the types of users that write in to make more informed changes to the docs, UI and the various products we offer. Moreover, these metrics would help us improve how we provide support. A key part of getting good data from our helpdesk was making sure to make more informed changes to the docs, UI and the various products we offer. Moreover, these metrics would help us improve how we provide support. The problem with this is that it requires everyone to remember to do that.


Here at Netlify, we use Intercom as our helpdesk, and we’ve even written several custom integrations for it. The main purpose of doing so is to extend its functionality beyond the API and webhooks that Intercom already provides. In this manner, we can then get relevant data into all new cases automatically without the need for manual tagging. In this post, we will specifically cover how we are enforcing tagging of cases.

My tech stack for this project is rather simple. I’m using a Javascript lambda function that’s triggered via webhook when someone closes an Intercom conversation. Intercom expects a 200/204 response code in response to the webhook. Otherwise, they assume that it’s erroring out.

Intercom considers every response in a conversation a “conversation part”. Each “conversation part” includes just that one element of dialog, the author, tags, as well as any attachments on it. Their conversation closing API only includes the conversation part for the last interaction before the conversation was closed. This means that it will only include the tags from that single part, and won’t include tags from earlier parts of that conversation. As a result, we’ll have to make a query to their API to get the complete tag list for the conversation.

The only dependency that I’m using is Axios, which is an HTTP client. I prefer Axios over fetch for a couple of reasons, one of them being the automatic parsing of JSON, and the other is better error handling (they throw errors for non-200 status code responses).

This is a simplified timeline of what happens in this function:

The function begins with saving data from the webhook to some variables, as well as my intercom API token that’s saved in an environment variable:

const eventBody = JSON.parse(event.body);const { assignee } = eventBody.data.item;const intercomHeaders = {'Content-Type': 'application/json',Accept: 'application/json',Authorization: `Bearer ${process.env.INTERCOM_SECRET}`,};

All of this is done inside of the lambda handler function, which is whereevent comes from. event includes the payload from the webhook, so we’ll pull out the person that conversation is assigned to so we can use it later on in the function. I’m also declaring the headers that I’ll use in my HTTP requests to Intercom’s API.

Since not every conversation is meant to be handled in the same way, I created multiple replies and saved them to variables. To understand the response body you probably should read up on Intercom’s reply API. But the most relevant part is:

Intercom reply API

These are the responses that I created for our use cases:

const noteBodyOpen = {type: 'admin',message_type: 'open',admin_id: '1669197',};const noteBodyComment = {type: 'admin',message_type: 'note',admin_id: '1669197',body: `Looks like a human forgot to tag this conversation.You can find info on how we categorize tickets at https://internal-docs.netlify.com/meta/ticket-categorization/`,};

All of them are “admin” responses, meaning they come from an administrator account and not a user account. The admin_idwe use for all automated responses is a bot user that we created for that purpose. We do this to make it clear to users and admin that they are actually automated responses. We do the same thing for private notes. The message_type is note, which tells intercom that these are internal notes that are only visible to admins and not the users. The body is the content of the note that the admin sees after our function re-opens the closed conversation.

I actually also use this same function to reassign cases that are closed while in the wrong queue. That way we make sure that closed cases go back to the proper queue and don’t get lost. However, that’s beyond the scope of this article so I’ve removed the sections that do that.

Next, we define the method that responds to the webhook so that intercom knows we got it and that it didn’t error out. We always return a 200 in this case.

const respond = () => {callback(null, {statusCode: 200,body: JSON.stringify('I got it!'),});};

We then define a method to make POST requests to Intercom’s API. The payload for the request is passed into the method since it’s different depending on what we’re trying to do. Just trying to stay DRY here.

One other thing to note is I’m not doing anything with the response from the POST request. The function is triggered in response to a webhook and it’s rather simple and doesn't do anything else so there isn’t any real error handling other than logging the error to the console.

const pingIntercom = async body => {try {const _ = await axios.post(`https://api.intercom.io/conversations/${eventBody.data.item.id}/reply`,JSON.stringify(body),{ headers: intercomHeaders });respond();} catch (err) {console.log(err);respond();}};

This following method is called with the tags array as an argument. The logic in it is specific to our use-case, but what it does is check if the conversation is assigned to our paid queue or the unnassigned queue. It also checks if the tags array has an element in the first position (to see if it has any tags in the tag array). That could have also been rewritten to check the length of the array.

While this function can’t be copied over exactly due to the names being specific to our workflow, its logic can be used to make sure you are only enforcing tags in specific mailboxes. If you have other people in the company using Intercom in different ways and don’t need tag enforcement, then you don’t want all their conversations being re-opened!

const replyToConvo = tags => {if ((assignee.name === 'Paid customers' || assignee.type === 'nobody_admin') && !tags[0]) {pingIntercom(noteBodyOpen);pingIntercom(noteBodyComment);}respond();};

To wrap this all up, we have the final function, which is the first one that’s called. Its purpose is to get the complete tag list since that’s necessary before we can do anything with the conversation.

const getTagList = async () => {try {const response = await axios.get(`https://api.intercom.io/conversations/${eventBody.data.item.id}`, {headers: intercomHeaders,});console.log(response.data.tags.tags);replyToConvo(response.data.tags.tags);} catch (err) {console.log('there was a problem fetching the full conversation');console.log(err);}};getTagList();

We call it immediately after it’s defined, so we can refactor it to be an IIFE:

(async () => {try {const response = await axios.get(`${BASE_URL}/${eventBody.data.item.id}`, {headers: intercomHeaders,});console.log(response.data.tags.tags);replyToConvo(response.data.tags.tags);} catch (err) {console.log('there was a problem fetching the full conversation');console.log(err);}})();

And we’re done! But you don’t have to be. You can add more functionality, like enforcing that conversations are closed while in the correct mailbox. For instance, we open conversations to invite people into beta programs or to separate conversations related to emergencies from the regular queue’s. If you don’t open these queues that often then you may want to make sure that conversations are never closed while they are in them. Or if you want conversations that are closed moved out of personal queues and into an unnassigned queue so if someone takes a day off, you don’t have responses to their conversations getting lost until they come back to work. There are lots of use cases and I’d love to hear some of yours!

Gerald Onyango

Written by

Avid reader. Support and developer @ Netlify. I geek out over everything.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade