Invoke remote API from forge application

Hi,

I’m currently developing an UI kit issue panel app using Atlassian Forge, and I am having trouble integrating Twilio’s WhatsApp API. I’m trying to send WhatsApp messages from Jira using Twilio, but my current approach cannot seem to authenticate properly and is throwing a 401 error

Error sending WhatsApp message: Error: 
Twilio API request failed with status 401 
at Object.sendWhatsAppMessage (webpack://jira-issue-panel-ui-kit/src/index.jsx:111:1) 
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) 
at async  (webpack://jira-issue-panel-ui-kit/node_modules/@forge/ui/out/reconcile.js:31:1) 
at async asyncMap (webpack://jira-issue-panel-ui-kit/node_modules/@forge/ui/out/reconcile.js:13:1) 
at async  (webpack://jira-issue-panel-ui-kit/node_modules/@forge/ui/out/reconcile.js:34:1) 
at async asyncMap (webpack://jira-issue-panel-ui-kit/node_modules/@forge/ui/out/reconcile.js:13:1) 
at async  (webpack://jira-issue-panel-ui-kit/node_modules/@forge/ui/out/reconcile.js:96:1) 
at async asyncMap (webpack://jira-issue-panel-ui-kit/node_modules/@forge/ui/out/reconcile.js:13:1) 
at async  (webpack://jira-issue-panel-ui-kit/node_modules/@forge/ui/out/reconcile.js:34:1) 
at async asyncMap (webpack://jira-issue-panel-ui-kit/node_modules/@forge/ui/out/reconcile.js:13:1)

Here’s a brief overview of my setup:

import {invokeRemote} from '@forge/api';

const App = () => {

const sendWhatsappMessage = async () => {

const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const messageBody = "Hello! This is a WhatsApp message from Jira.";


const authHeader = 'Basic ' + Buffer.from(accountSid + ':' + authToken).toString('base64');

try {
    console.log("Before making the Twilio API call");
    const response = await invokeRemote('twilio-remote', {
        path: `/2010-04-01/Accounts/${accountSid}/Messages.json`,
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Authorization': authHeader
        },
        body: new URLSearchParams({
            From: 'whatsapp:+987654321',
            To: 'whatsapp:+123456789',   
            Body: messageBody
        }).toString()
    });

    console.log("Twilio API call made, checking response...");

    if (!response.ok) {
        throw new Error(`Twilio API request failed with status ${response.status}`);
    }

    const jsonResponse = await response.json();
    setMessageStatus(`WhatsApp message sent: ${JSON.stringify(jsonResponse)}`);
    console.log(`WhatsApp message sent: ${JSON.stringify(jsonResponse)}`);
} catch (error) {
    setMessageStatus(`Error sending WhatsApp message: ${error.message}`);
    console.error('Error sending WhatsApp message:', error);
}
};

This is included in my manifest.yml:

remotes:
  - key: twilio-remote
    baseUrl: 'https://api.twilio.com'
    operations:
      - compute

I’m unsure if I’m setting it up correctly, particularly with handling request headers and body formatting for external API integration. My environment variables TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN seems to be set correctly, I am using the credentials from Twilio sandbox.

Could anyone with experience in integrating external APIs, especially Twilio, with Forge apps advise on best practices or what might be going wrong? Also, any insights into configuring the manifest.yml for remote APIs would be greatly appreciated.

Thanks in advance!

bump.

Hi @SimonLarson, sorry we didn’t get to your question earlier.

I don’t think you should be using Forge remote for this use case as Forge remote will override your authorization header with a Forge Invocation Token (FIT). For your use case, you should just define an egress permission and make a request to the Twilio API using the fetch API from @forge/api or using the Global fetch (provided you are on the native Node runtime).

4 Likes

Hi @BoZhang, thank you for your suggestion. I tried adding an egress permission to my manifest:

permissions:
  external:
    fetch:
      backend:
        - '*.api.twilio.com'

As well as changing from remoteinvoke to fetch:


import { fetch } from '@forge/api';
const sendWhatsAppMessage = async () => {

    const accountSid = process.env.TWILIO_ACCOUNT_SID;
    const authToken = process.env.TWILIO_AUTH_TOKEN;
    const messageBody = "Hello! This is a WhatsApp message from Jira.";

    const authHeader = 'Basic ' + Buffer.from(accountSid + ':' + authToken).toString('base64');

    try {
        console.log("Before making the Twilio API call");
        const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${accountSid}/Messages.json`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Authorization': authHeader
            },
            body: new URLSearchParams({
                From: 'whatsapp:+123456789',
                To: 'whatsapp:+987654321',
                Body: messageBody
            }).toString()
        });

        console.log("Twilio API call made, checking response...");

        if (!response.ok) {
            throw new Error(`Twilio API request failed with status ${response.status}`);
        }

        const jsonResponse = await response.json();
        setMessageStatus(`WhatsApp message sent: ${JSON.stringify(jsonResponse)}`);
        console.log(`WhatsApp message sent: ${JSON.stringify(jsonResponse)}`);
    } catch (error) {
        setMessageStatus(`Error sending WhatsApp message: ${error.message}`);
        console.error('Error sending WhatsApp message:', error);
    }
};

But I get the same 401 error as before. Am I calling fetch correctly? If so, maybe I should take a closer look at the twilio creditials, although they seem fine on the surface…

Hmm interesting, is the 401 error coming back from the Forge platform or are you getting an error back from Twilio?
Did you make sure to deploy the changes and upgrade your installation to the latest version (changes to egress will cause a major version bump)?
When you say that it works on the surface, do they work when you spin up a vanilla node.js application and try make the same call (don’t use fetch from @forge/api of course)?