Build an Auditable SMS Proxy Using Twilio Programmable SMS, Pangea Audit Service, and JavaScript

October 28, 2022
Written by
Nicolas Vautier
Opinions expressed by Twilio contributors are their own
Reviewed by

Header image

Third parties knowing what is said in SMS conversations can be scary, but in a world filled with increasingly false and misleading information being presented as news, not knowing what was said can be just as scary. In this tutorial, learn how to add cryptographically verifiable hashes of Twilio powered SMS conversations to an immutable public ledger or blockchain powered by Pangea’s tamperproof audit and logging service.

By publishing conversation hashes to the blockchain you can provide irrefutable evidence of what was said between two parties without actually exposing the contents of the conversation publicly. With Twilio Programmable SMS and Pangea’s Javascript SDK you can deliver the power of the blockchain to your users with a few lines of code. In this tutorial, you’ll set up a free Pangea account and, using Node.js, build an SMS forwarding service that logs messages between two parties using Twilio Functions.


  • A free or paid Twilio account
  • A Twilio phone number
  • A free Pangea account
  • Node.js (version 14.16.1 or higher)

Set up your Pangea account and Access Token

Once you’ve signed up for Pangea, log in and access the Pangea Console. Set up the Audit service by selecting Secure Audit Log from the left-hand navigation menu.

Pangea Console

Review the benefits of the service and select Next to continue.

Audit Service informational dialog

Create an Access Token by selecting a token name, expiration date, and token scope or use the default values by selecting Done.

Create token dialog

Make a note of the Config ID, service Domain, and access Token. You will use each of these values to interact with the service from your app’s code in the next step.

Note: You can quickly copy each value to your system’s clipboard using the shortcuts.

Audit service dashboard

Get the code

In this section, you will configure your app to communicate with Twilio, and Pangea, with your account credentials. You’ll also configure which phone numbers your proxy will forward each message to and examine the code that does so.

Navigate to your terminal and enter the following command to clone the SMS proxy app:

git clone

Now change the working directory to your new Node.js project, audit-twilio-proxy, with the following command:

cd audit-twilio-proxy

Open up your project directory in your preferred IDE and take a moment to explore the project files and configure it to your environments.

  • package.json - Describes the project and lists dependencies. Both the Twilio and Pangea SDKs for Node.js are listed. The devDependencies includes twilio-run which you’ll use to deploy to code to Twilio Functions.
  • .env - Contains the environment variables the app will reference.
  • functions/sms-audit-proxy.js - The application source file that contains a single function to handle incoming SMS messages.

Configure and deploy the app

The application source reads 7 variables from the .env file.

  • ACCOUNT_SID and AUTH_TOKEN to authenticate your app with the Twilio service.
  • PANGEA_DOMAIN, PANGEA_CONFIG_ID, and PANGEA_AUTH_TOKEN to authenticate with the Pangea service.
  • TARGET_NUMBER and OWNER_NUMBER are used to determine where to forward incoming SMS messages.

Modify the .env file by replacing each {REPLACE} tag with its corresponding value.

For the PANGEA_ specific variables, use the three values you noted in the previous section, or retrieve them from the Pangea Console by navigating to the Secure Audit Log tab.

The Twilio values for ACCOUNT_SID and AUTH_TOKEN can be found on the landing page of the Twilio Console.

OWNER_NUMBER and TARGET_NUMBER should each be a valid E.164 phone number you’d like to test with. For example, OWNER_NUMBER can be set to your mobile phone number and TARGET_NUMBER to a friend's number who you’d like to start and record an auditable message thread with. An example of an E.164 formatted number in the US is +16502223333.

Note: You can also set both OWNER_NUMBER and TARGET_NUMBER to your personal mobile number and reply to your own messages.

Verify your changes to the .env file with the git diff sub-command:

git diff

The output should look similar to this:

diff --git a/.env b/.env
index 7442c6a..9fd8304 100644
--- a/.env
+++ b/.env
@@ -1,7 +1,7 @@

Code walkthrough

Inspect the contents of the /function/sms-audit-proxy.js source file before deploying the app to a Twilio Serverless environment. The file contains a single function. You will configure Twilio to invoke this function when your Twilio owned number receives an SMS. The function’s response and actions taken will be determined by the contents of the object passed in as the event parameter.

exports.handler = function(context, event, callback) {

  console.log("Event:", event);

The user defined environment variables set in the .env file will be passed into the function via the context parameter. The Twilio account variables are read and set to accountSid and authToken, respectively, and used to represent an instance of the twilio class defined in the SDK.

  // Read the Twilio SID and Auth Token from the environment variables
  const accountSid = context.ACCOUNT_SID;
  const authToken = context.AUTH_TOKEN;

  // Import the Twilio SDK
  const TwilioClient = require('twilio')(accountSid, authToken);

TwilioClient is then used to create a MessagingResponse object which may be used to reply to the sender. Note that a reply will only be necessary when reporting an error back to the sender.


const twiml = new TwilioClient.twiml.MessagingResponse();

The Pangea SDK is imported and the PANGEA_ values are assigned to their respective variables. A PangeaConfig object is created and used to create an AuditService instance.

  // Import the Pangea SDK
  const Pangea = require('node-pangea');

  // Read the Pangea Config Id and Auth Token from the environment variables
  const pangeaDomain = context.PANGEA_DOMAIN;
  const auditToken = context.PANGEA_AUTH_TOKEN;
  const auditConfigId = context.PANGEA_CONFIG_ID;

  // Instantiate a Pangea Configuration object with the endpoint domain and configId
  const auditConfig = new Pangea.PangeaConfig({ domain: pangeaDomain,
        configId: auditConfigId});

  const auditService = new Pangea.AuditService(auditToken, auditConfig);

Before invoking the Audit Service, determine if the inbound message should be added to the secure audit log and if so who to relay the message to after it is logged. Only messages between the two numbers configured in the .env file should be logged. Each of the allowed recipient numbers are read into local variables.

  const ownerNumber = context.OWNER_NUMBER;
  const targetNumber = context.TARGET_NUMBER;

Determine the destinationNumber to relay the message to:

  • If the message originated from the owner, send it to the target.
  • If the message came from the target, send it to the owner.
  • If the message came from any other number, notify the sender that the message will be ignored by populating and returning the MessageResponse.
  var destinationNumber;

  // Determine the destination number
  if(event.From.endsWith(ownerNumber)) {
        // If the message is from the owner, send it to target
        destinationNumber = targetNumber;
  } else if(event.From.endsWith(targetNumber)) {
        // If the message is from the target, send it to owner
        destinationNumber = ownerNumber;
  } else {
        // If the message is from any other number, reply to the sender
        twiml.message("AUTOMATED RESPONSE: This is a private communication channel to securely record auditable conversations. Your message will be ignored!");
        return callback(null, twiml);

Create a JavaScript object and map the relevant event details to the log entry. Set the actor field to the Twilio owned number that is logging and forwarding the message. The source is set to the number that sent the incoming SMS, read from the event.From field, and target is set to the intended recipient or destinationNumber. The message is set to the body or contents of the SMS, which is also read from the event object.  

  // The number the original message was sent to is the number of the proxy
  var proxyNumber = event.To;

  // Map the event details to the auditData object, for example, the source
  // is set to the number that sent the message and the target is the recipient
  const auditData = {
        actor: proxyNumber,
        source: event.From,
        target: destinationNumber,
        message: event.Body,
        status: event.SmsStatus,
        action: "forwarded"

Call the log method of the AuditService with the auditData. This will invoke the Pangea Service to create the log entry. The hash of the message will also be recorded on a tamper proof blockchain which can then be used to prove the conversation has not been modified. You can explore the log viewer on the Pangea Console to verify the message integrity later.

  // Log the message using the Pangea Audit service. Hashes of each message will
  // be recorded on a tamper proof blockchain so the conversation can be
  // cryptographically proven to be unmodified.
  auditService.log(auditData, auditOption)
        .then(function(response) {


When the asynchronous log function completes the callback function is called with a single response parameter. If the response is marked successful, use the TwilioClient to relay the contents of the message to the destinationNumber and return a blank response to complete the function execution.

if(response.success) {

  console.log("Forwarding message to: ", destinationNumber);

  // Send the logged message to the destination number
    .create({body: event.Body, from: proxyNumber, to: destinationNumber})
    .then((response) => {

      console.log('SMS successfully sent');

      return callback(null, twiml);
    .catch((error) => {
      return callback(error);


Deploy to Twilio Functions & Assets

Twilio Functions is a serverless environment that empowers developers to quickly create and host applications. Because the package.json file lists the twilio-run module as a development dependency, you can quickly deploy your function using the following command:

npx twilio-run deploy

The module uses the same ACCOUNT_SID and AUTH_TOKEN used by the application in the .env file to authenticate with your Twilio account when deploying the project.

A successful deploy will yield output similar to:

Deploying functions & assets to the Twilio Runtime

Password    fXXX****************************
Environment    dev
Root Directory    /Users/Pangea/Development/Source/twilio-audit-proxy
Dependencies    twilio, @twilio/runtime-handler, node-pangea
Runtime            node14

✔ Serverless project successfully deployed

Deployment Details
Build SID:
View Live Logs:

Congratulations! You just deployed a Pangea enabled Twilio service. Make a note of the Functions URL as it will be needed to configure your Twilio programmable number.

If you do not already have a Twilio number, follow these instructions:

  • Go to your Phone Numbers Dashboard.
  • Click Buy a Number.
  • Search for a number that suits you.
  • Click Buy
  • Confirm your purchase, then click Setup Number.

Buy a number page in the Twilio console

Otherwise, navigate to the Active numbers panel of the Twilio console and select the number you’d like to use with this service.

Active numbers page in the Twilio console

Under Messaging, look for the line that says “A message comes in.” Change the first box to “Webhook” and add the function URL to the second box, then click Save to save your configuration. The function you deployed will now be invoked every time an SMS is sent to this number.

Configure a phone numbers webhooks page in the Twilio console

Test and verify the conversation audit trail

That's it! You now have a secure audit trail between the OWNER_NUMBER and TARGET_NUMBER you configured in the .env file. Use a cellphone with either number to send an SMS to the Twilio number you purchased and configured to invoke your function. The SMS message will be logged by the Pangea service and forwarded to the other participant, similarly their replies will be forwarded back to you creating a conversation thread on the SMS apps on both your phones.

A conversation thread on a mobile device

To later view the conversation or present the verified proof that messages were not altered or deleted, navigate back to the Pangea Console, select Secure Audit Log from the left-hand navigation menu, and then select View Logs.

Secure Audit Log Viewer

You can expand each message to view the sender and recipient details, labeled as source and target, respectively.

Secure Audit Log viewer message details

The green lock to the left of each message indicates that its hash has been published to the ARWeave blockchain and verified. To view the transaction on, click the lock icon and then click the View button next to Published Root.

Note: Publishing occurs once per hour so it may take up to an hour for a record to show as Verified.

Secure Audit Log viewer audit details


In this article you learned how to build and deploy an SMS proxy that records conversations and provides users with verifiable proof that the conversation was not altered. Most telecom providers maintain records of the messages sent over their network, but even the largest carriers are susceptible to tampered records on their centralized data sources. It may also be impractical for your users to request conversation logs from carriers as the process may require a legal process or subpoena. Utilizing a Blockchain with your Twilio and Pangea powered proxy you are able to quickly solve both these problems for your user.

Nicolas Vautier is a Developer Advocate at Pangea Cyber and is a privacy and data security enthusiast. If you have questions, comments, or ideas for future posts, Nicolas can be reached at Follow him on Twitter @DeveloperEnvY to join his journey as he helps unify security services for app builders @PangeaCyber.