The Twilio Webhook is only needed if you are doing a custom integration with Twilio. If you are using the Messaging Service to send and receive SMS messages, you don't need to implement the webhook in this example. The Messaging Service handles incoming messages for you, creating Communication FHIR resources for each one. With the Messaging Service, you can react to newly created FHIR Communication resources by creating a a Subscription Zambda that fires when a new FHIR Communication resource is created.
Twilio SMS Webhook
When someone texts your Twilio (opens in a new tab) phone number, Twilio sends the message to your messaging webhook (opens in a new tab) and expects a TwiML (opens in a new tab) response that tells it what to do next, such as texting back a reply. This example builds a Zambda in raw mode that receives text messages from Twilio, checks that they really came from Twilio, and replies with TwiML.
Why Raw Mode?
Twilio expects TwiML as the response: an XML document whose root element is <Response>, with an XML Content-Type such as text/xml. In raw mode, Twilio receives the TwiML as your Zambda returns it:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>Thanks for your message! We will get back to you soon.</Message>
</Response>Without raw mode, Oystehr wraps the same response in its standard structure:
<response>
<status>200</status>
<output><?xml version="1.0" encoding="UTF-8"?><Response>...</Response></output>
</response>That isn't valid TwiML: the root element is <response>, and the TwiML's XML declaration ends up in the middle of the document. Twilio logs a document parse failure (error 12100 (opens in a new tab)) instead of sending your reply.
How It Works
- Someone texts your Twilio phone number.
- Twilio sends the message to your Zambda's URL as an HTTP
POSTwith form-encoded parameters, such asFrom,To, andBody. Twilio signs the request with your Twilio account's auth token and includes the signature in theX-Twilio-Signatureheader. - Your Zambda checks the signature (opens in a new tab), handles the message, and responds with TwiML.
- Twilio follows the TwiML, for example by texting your reply to the sender.
Setup Instructions
1. Write the Zambda
Create a project for the Zambda and install Twilio's Node.js library, which the Zambda uses to check Twilio's signature and build TwiML:
mkdir twilio-sms-webhook && cd twilio-sms-webhook
npm init -y
npm install twilioCreate a file called index.js with this code:
const twilio = require('twilio');
// Header names can arrive in any case, so look them up case-insensitively
const getHeader = (headers, name) => {
const key = Object.keys(headers ?? {}).find((header) => header.toLowerCase() === name.toLowerCase());
return key ? headers[key] : undefined;
};
exports.index = async (event) => {
const { TWILIO_AUTH_TOKEN, TWILIO_WEBHOOK_URL } = event.secrets ?? {};
if (!TWILIO_AUTH_TOKEN || !TWILIO_WEBHOOK_URL) {
throw new Error('The TWILIO_AUTH_TOKEN and TWILIO_WEBHOOK_URL secrets must be set');
}
// Twilio sends the message as form-encoded parameters, such as From, To, and Body
const params = Object.fromEntries(new URLSearchParams(event.body ?? ''));
// Only handle requests that Twilio signed with your account's auth token
const signature = getHeader(event.headers, 'X-Twilio-Signature');
if (!signature || !twilio.validateRequest(TWILIO_AUTH_TOKEN, signature, TWILIO_WEBHOOK_URL, params)) {
return {
statusCode: 403,
headers: { 'Content-Type': 'text/plain' },
body: 'Invalid Twilio signature',
};
}
// Handle the message here, for example by saving it as a FHIR Communication
console.log(`Received message ${params.MessageSid}`);
// Reply with TwiML. To receive the message without replying, leave out twiml.message()
const twiml = new twilio.twiml.MessagingResponse();
twiml.message('Thanks for your message! We will get back to you soon.');
return {
statusCode: 200,
headers: { 'Content-Type': 'text/xml' },
body: twiml.toString(),
};
};The Zambda reads your Twilio auth token and its own URL from Zambda Secrets, which you'll add in step 3. It needs the URL because Twilio's signature covers the URL that Twilio sent the request to.
Zip the code together with its dependencies:
zip -r twilio-sms-webhook.zip index.js node_modules2. Deploy the Zambda in Raw Mode
Create the Zambda with the http_open trigger method, since Twilio can't send an Oystehr access token, and with raw mode turned on. Then upload the zip file.
- Navigate to Services > Zambdas > Functions (opens in a new tab)
- Click "New Zambda"
- Set the Name to "twilio-sms-webhook"
- Set the Trigger method to "http_open"
- Set the Runtime to "nodejs22.x"
- Check "Raw mode"
- Upload twilio-sms-webhook.zip
- Click "Deploy Zambda"
Your Zambda's URL, which you'll give to Twilio, is:
https://zambda-api.zapehr.com/v1/zambda/<zambda_id>/execute-publicIn the Developer Console, you can copy it with the "Copy Zambda API URL" button on the Zambda's page.
3. Add Your Twilio Credentials to the Project's Secrets
Add two secrets:
TWILIO_AUTH_TOKEN: the auth token of the Twilio account that owns your phone number, from the Account Info section of the Twilio Console.TWILIO_WEBHOOK_URL: your Zambda's URL from step 2. It must exactly match the URL you enter in Twilio in step 4.
- Navigate to Services > Zambdas > Secrets (opens in a new tab)
- Click "New Secret"
- Set the Key to "TWILIO_AUTH_TOKEN" and the value to your Twilio auth token
- Click "Create Secret"
- Repeat for "TWILIO_WEBHOOK_URL", with your Zambda's URL as the value
4. Send Your Phone Number's Messages to the Zambda
In the Twilio Console:
- Go to Phone Numbers > Manage > Active numbers and select your phone number
- In the phone number's messaging configuration, set "A message comes in" to "Webhook"
- Set the URL to your Zambda's URL, and the HTTP method to "HTTP POST"
- Save the configuration
If your phone number belongs to a Twilio Messaging Service, the Messaging Service's integration settings may decide where incoming messages are sent. In that case, set the Messaging Service's incoming message webhook to your Zambda's URL instead.
5. Test the Zambda
Text your Twilio phone number. You should get the reply "Thanks for your message! We will get back to you soon."
You can also check that your Zambda is in raw mode by calling it without a Twilio signature:
curl -i -X POST 'https://zambda-api.zapehr.com/v1/zambda/<zambda_id>/execute-public' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'Body=Hello'The response should have the status code 403 and the plain text body Invalid Twilio signature, exactly as your Zambda returned them.
If you don't get a reply to your text message:
- Check your Zambda's logs in the Developer Console (opens in a new tab).
- Check the Twilio Console's error logs for errors Twilio reported when it called your Zambda.
- If your Zambda responded with
Invalid Twilio signature, check thatTWILIO_AUTH_TOKENis your Twilio account's auth token, and thatTWILIO_WEBHOOK_URLexactly matches the URL you entered in Twilio.
Customizing the Reply
The Zambda builds its reply with the Twilio library's MessagingResponse, which escapes the text for you, so it is safe to include details from the incoming message:
twiml.message(`We received your message: "${params.Body}"`);To receive messages without replying, don't call twiml.message(). The Zambda then responds with an empty TwiML response, which tells Twilio to take no further action:
<?xml version="1.0" encoding="UTF-8"?><Response/>See Twilio's TwiML for Programmable Messaging (opens in a new tab) for everything a reply can include, such as media.