Making an API request and formatting the results in a Flow Function

For fetching data from an external source, you can use the Fetch Variables step in Flow Builder to fetch data and pass it to a function for parsing, using the Call Function step.

However, it is sometimes easier to combine the data fetching with some formatting or filtering logic inside a function.

This is what we will do in this example. We’re going to create a function that grabs all of the messages from a single conversation, formats them, and returns a single conversation transcript.

Here’s how we will set that function up:

  1. The function receives a conversation ID. (In flows using the omnichannel trigger, this will be available as a variable.)

  2. The function makes a request to the MessageBird Conversations API.

  3. If the request is successful, the function will receive and parse the response.

  4. The function reviews and formats the messages, based on the type of message (text, image, location, etc.)

  5. The function returns the formatted transcript as raw data, ready for us to forward to an HTTP endpoint via a flow, and also generate a Markdown table from the data so we can share it as text.

Building our function

Add required dependencies

We’ll start by adding the required dependencies as follows:

  1. To make the HTTP request to the Conversations API, use the `axios` NPM module. While NodeJS (the JavaScript runtime functions use) also contains a native HTTP module, axios provides us with a more user-friendly interface. Install the latest version available. At the time of writing, this is version 0.2.1.1.

  2. Next, to generate a Markdown table from our data we’ll use the `tablemark` NPM module. This is a small library that takes a JavaScript array data structure and returns a string for the markdown table. Install the latest version available. At the time of writing, this is version 2.0.0.

Set up environment variables

Now, we need to set up environment variables for this function. When we make the request to the MessageBird Conversations API we’ll need to authorize ourselves using an API key (access key).

We use an environment variable for this API key as it is something that we want to keep secret since it gives access to our Messagebird account, so we do not want to put it into our code in plain text.

Follow these steps to do this:

  1. Go to your dashboard on the API Access page.

  2. Create a new key or copy an existing one, making sure the mode is “live”.

  3. Add it into the “Environment variables” tab of the function editor.

  4. Name your key. We named our `ACCESS_KEY`, but you can choose any name you like (make sure to update the code accordingly). Using all uppercase makes it easier to identify the environment variables in our code.

Define input parameters

Next, we need to define our input parameters. We only need to define one for our function: `conversationId`.

Let’s break down the code

The entire code for our function will look like this. Note that we’ve moved some helper functions outside of the main `exports.handler` method.

'use-strict';

const axios = require('axios');
const tablemark = require('tablemark')

exports.handler = async function(context, variables) {
    if (!variables.conversationId) {
        throw new Error(`conversationId variable is missing!`);
    }

    if (!context.env.ACCESS_KEY) {
        throw new Error(`Access key missing from env vars`)
    }
    
    const resp = await axios({
        method: 'GET',
        url: `https://conversations.messagebird.com/v1/conversations/${variables.conversationId}/messages`,
        headers: {
            'Authorization': `AccessKey ${context.env.ACCESS_KEY}`,
            'Accept': 'application/json'
        }
    });

    if (resp.status > 200 && resp.status < 300) {
        throw new Error(`Failed to get conversation messages`, resp.status);
    }
    
    const messages = resp.data.items
    // copy array for immutability
    .slice()
    // reverse array order so order equals that of conversation
    .reverse()
    // filter out inbox internal event type messages
    .filter(item => item.type !== 'event');

    // All table entries should have format {name: string, content: string; date: string}
    const tableData = messages.map(message => ({
        name: message.from,
        content: getMessageContent(message),
        date: formatDate(message.createdDatetime)
    }))
    
    return {
        rawMessages: messages,
        tableData,
        markdown: tablemark(tableData),
    }

};

const getMessageContent = (message) => {
    let content;
    switch (message.type) {
        case 'text':
            content = message.content.text;
            break;
        case 'image':
            content = message.content.image.url;
            break;
        case 'video':
            content = message.content.video.url;
            break;
        case 'audio':
            content = message.content.audio.url;
            break;
        case 'file':
            content = message.content.file.url;
            break;
        case 'location':
            content = `${message.content.location.latitude} x ${message.content.location.longitude}`;
            break;
        default:
            console.log(`Unknown message type: ${message.type}`)
            content = 'unknown';
            break;
    }
    return content;
}

const formatDate = (datestamp) => new Date(datestamp).toLocaleString('nl-NL', {
    timezone: 'Amsterdam/Europe'
})

Let’s go through it step by step. At the top of the function, this code imports our dependencies so that we can use them in our function.

const axios = require('axios');
const tablemark = require('tablemark'

Next, inside the `exports.handler` method, we make sure we’ve received our external inputs: `conversationId` from variables and `ACCESS_KEY` from the environment variables.

 if (!variables.conversationId) {
    throw new Error(`conversationId variable is missing!`);
}

if (!context.env.ACCESS_KEY) {
    throw new Error(`Access key missing from env vars`)
}

If anything is missing, we “throw” an error, meaning our function will crash. The error messages will show up in the logs together with a “stack trace”, an indication of where in our code the error occurred.

After this, we make our HTTP request using axios:

const resp = await axios({
        method: 'GET',
        url: ``,
        params: {
            limit: 20,
            offset: 0,
        },
        headers: {
            'Authorization': `AccessKey ${context.env.ACCESS_KEY}`,
            'Accept': 'application/json'
        }
    });

The details for this request are based on the Conversations API documentation. In the documentation, it shows that we need to make a `GET` request to the `/conversations/{conversationId}/messages` endpoint. Of course in our code, we fill in the conversation ID that we received from the variables. We also set the params `offset` and `limit`. By setting offset to 0 and limit to 20 (the maximum) we get the first 20 messages of the conversation. For simplicity, we’ll assume that we won’t have more messages per conversation here. If we did, we’d have to fetch multiple “pages” of items and make multiple requests where we increase the offset.

We also need to set some headers according to the documentation. First, the `Authorization` header contains the access key to authorize our requests. This allows us to access data from our Messagebird account. Second, we set the `Accept` header to `application/json` since this is the data format we expect to receive back.

Also, note the `await` keyword. This in combination with the `async` keyword at the very start of our function makes sure we don’t execute further steps until the request is done and a response is received. The `await` keyword here is needed because HTTP requests are asynchronous by default. Since we need the response to execute the rest of our function, it is important that we wait for it. For more details on this syntax and asynchronous execution of functions, see the async/await documentation.

After making the request, we validate if the status code we received is between 200 and 300. We know it has to be within this range because the Conversation API documentation specifies that it will respond with status codes in the “2xx range” if the request is successful. This means that if our code is in this case, we can safely assume that we’ve received our message data. If the code is different, we throw an error and stop the function execution.

Once we get the data, we do some basic ordering and filtering of the list items. We read our `resp.data.items` because we want the data from the HTTP response. In this data, there is an items property that contains the list of messages. This is detailed in the API documentation.

const messages = resp.data.items
    // copy array for immutability
    .slice()
    // reverse array order so order equals that of conversation
    .reverse()
    // filter out inbox internal event type messages
    .filter(item => item.type !== 'event');

First, we call the `slice` method on the list (array) of messages to make a copy. This ensures that we do not alter the original list when we call our next method, `reverse`. We reverse the order of the list because the messages are listed from newest to oldest, but for our transcript, we want the oldest message first. Lastly, we call `filter` on our list and filter out any messages of type “event”. This is an internal message type used by MessageBird Inbox to track events connected to conversations. For our transcript we’re not interested in these messages, so we filter them out.

Next, we parse our message items one by one into a format of name, content, and date.

const tableData = messages.map(message => ({
        name: message.from,
        content: getMessageContent(message),
        date: formatDate(message.createdDatetime)
    }))

To format the message content and the date we have separate methods which we go over later. The reason to have the content method is that we want different data per message type. For example for text messages we want the `message.content.text` property while for images we want `message.content.image.url` as the content value. For the date, the function transforms an RFC 3339 standard timestamp into a more human-readable and timezone specific format.

At the end of the handler function we return several properties we can use as variables in our flow:

 return {
        rawMessages: messages,
        tableData,
        markdown: tablemark(tableData),
    }

`rawMessages` contains the message data as we received it from the API, but with the list reversed and filtered for event type messages. `tableData` contains the list of formatted messages. We could use this if we wanted to forward the transcript to another HTTP endpoint. Lastly, `markdown` contains the markdown string of our `tableData`. We could use this if we want to display the transcript table, for example in an email.

Now, let’s take a quick look at the helper methods. The `getMessageContent` helper function receives a message data structure and returns a string for the content. We use a switch statement here where we check the `message.type` value and based on that return the property we want. Note that for location messages we combine the latitude and longitude into a single message. These properties per message type can all be found in the Conversations API documentation. Also, if we find a type we have no case for, we return “unknown” and create a log containing the type so we can see what type we did not handle.

The `formatDate` helper takes a RFC 3339 standard timestamp as input (in our case `message.crecreatedDatetimeatedAt`) and returns a formatted date/time stamp. It first creates a JavaScript Date object and then transforms that to a locale string using the `toLocaleString` method. As options here we can specify a “locale” (e.g. nl-NL, en-GB, en-US, etc.) as well as the time zone. Passing the time zone will transform the time from UTC time to the specified time zone’s time. Of course to do more advanced parsing we could also use the `date-fns` module again here as described above but for the example, we keep it simple.

Testing the function

To test the function we’ll need a conversation ID. One way to obtain this is to look at the logs of an Omnichannel flow. As part of the variables, you should see `conversation id`. The value would be a unique ID string such as “4865285aacd846a3a5570490e9ce7406”. Copy this value and paste it into the test modal for the function.

After running the function, you should see a long output value. If you scroll through it, it will first list the `rawMessages` variable containing all the API message data. Next, it will show the `tableData` which will be the same list but nicely formatted with name, content, and date. And at the bottom, we’ll see the `markdown` variable which contains the same data on a single line. This may look a bit odd but if we print this out from our flow it will look like a table!

Use the function with the Call Function step

To use our function in Flow Builder, we can add the Call Function step and configure it like this:

For this example, we named the output variable `myOutput`, but you can choose any name you like for this. To read out the values in the flow you can use `{{myOutput.rawMessage}}`, `{{myOuput.tableData}}` and `{{myOutput.markdown}}`.

Lastly, some notes and considerations for customizing the logic from the example:

  • In case there are more types of messages you want to filter out you can extend the part where we filter out “event” type messages.

  • If you want to handle cases where your function fails, make sure to use the “Handle failures” option on the Call Function step.

  • If you are looking for other ways to loop through and/or transform your array (list) data, have a look at the JavaScript array documentation. For example, you can use the `find` method to look for a single entry or `sort` to sort your data.

  • Of course, you can also use `axios` to make requests to other APIs. Make sure to carefully read the documentation so you set the method, URL, headers, and params correctly. Also, don’t forget the `await` keyword to make the code execute synchronously.

  • Make sure to always store sensitive information in environment variables. This prevents accidentally exposing this information to others, for example, if you copy/paste your code.

We hope the above example use cases provide some guidance and inspiration to create your own custom functions. If you have any feedback about them or other use cases you need help with, please reach out to our customer support.

Last updated