This example uses headers and cookies, which are only accessible when your Function is running @twilio/runtime-handler
version 1.2.0
or later. Consult the Runtime Handler guide to learn more about the latest version and how to update.
Protecting your Twilio Functions from non-Twilio requests is usually just a matter of setting a Function's visibility to Protected
. However, if you'd like to create a Function that's intended to only handle incoming Webhook requests from a product such as SendGrid, validation will require some manual inspection of headers, which are now accessible!
In this example, we'll create a Function which will serve as the Event Webhook for your SendGrid account. The Function will validate if the incoming request came from SendGrid, and send a text message to a designated phone number if an email has been opened.
In order to run any of the following examples, you will first need to create a Function into which you can paste the example code. You can create a Function using the Twilio Console or the Serverless Toolkit as explained below:
If you prefer a UI-driven approach, creating and deploying a Function can be done entirely using the Twilio Console and the following steps:
https://<service-name>-<random-characters>-<optional-domain-suffix>.twil.io/<function-path>
test-function-3548.twil.io/hello-world
.
Your Function is now ready to be invoked by HTTP requests, set as the webhook of a Twilio phone number, invoked by a Twilio Studio Run Function Widget, and more!
_102const { EventWebhook, EventWebhookHeader } = require('@sendgrid/eventwebhook');_102_102// Helper method for validating SendGrid requests_102const verifyRequest = (publicKey, payload, signature, timestamp) => {_102 // Initialize a new SendGrid EventWebhook to expose helpful request_102 // validation methods_102 const eventWebhook = new EventWebhook();_102 // Convert the public key string into an ECPublicKey_102 const ecPublicKey = eventWebhook.convertPublicKeyToECDSA(publicKey);_102 return eventWebhook.verifySignature(_102 ecPublicKey,_102 payload,_102 signature,_102 timestamp_102 );_102};_102_102exports.handler = async (context, event, callback) => {_102 // Access a pre-initialized Twilio client from context_102 const twilioClient = context.getTwilioClient();_102 // Access sensitive values such as the sendgrid key and phone numbers_102 // from Environment Variables_102 const publicKey = context.SENDGRID_WEBHOOK_PUBLIC_KEY;_102 const twilioPhoneNumber = context.TWILIO_PHONE_NUMBER;_102 const numberToNotify = context.NOTIFY_PHONE_NUMBER;_102_102 // The SendGrid EventWebhookHeader provides methods for getting_102 // the necessary header names._102 // Remember to cast these header names to lowercase to access them correctly_102 const signatureKey = EventWebhookHeader.SIGNATURE().toLowerCase();_102 const timestampKey = EventWebhookHeader.TIMESTAMP().toLowerCase();_102_102 // Retrieve SendGrid's headers so they can be used to validate_102 // the request_102 const signature = event.request.headers[signatureKey];_102 const timestamp = event.request.headers[timestampKey];_102_102 // Runtime injects the request object and spreads in the SendGrid events._102 // Isolate the original SendGrid event contents using destructuring_102 // and the rest operator_102 const { request, ...sendGridEvents } = event;_102 // Convert the SendGrid event back into an array of events, which is the_102 // format sent by SendGrid initially_102 const sendGridPayload = Object.values(sendGridEvents);_102_102 // Stringify the event and add newlines/carriage returns since they're expected by validator_102 const rawEvent =_102 JSON.stringify(sendGridPayload).split('},{').join('},\r\n{') + '\r\n';_102_102 // Verify the request using the public key, the body of the request,_102 // and the SendGrid headers_102 const valid = verifyRequest(publicKey, rawEvent, signature, timestamp);_102 // Reject invalidated requests!_102 if (!valid) return callback("Request didn't come from SendGrid", event);_102_102 // Helper method to simplify repeated calls to send messages with_102 // nicely formatted timestamps_102 const sendSMSNotification = (recipientEmail, timestamp) => {_102 const formattedDateTime = new Intl.DateTimeFormat('en-US', {_102 year: 'numeric',_102 month: 'numeric',_102 day: 'numeric',_102 hour: 'numeric',_102 minute: 'numeric',_102 second: 'numeric',_102 hour12: true,_102 timeZone: 'America/Los_Angeles',_102 }).format(timestamp);_102_102 return twilioClient.messages.create({_102 from: twilioPhoneNumber,_102 to: numberToNotify,_102 body: `Email to ${recipientEmail} was opened on ${formattedDateTime}.`,_102 });_102 };_102_102 // Convert the original list of events into a condensed version for SMS_102 const normalizedEvents = sendGridPayload_102 .map((rawEvent) => ({_102 to: rawEvent.email,_102 timestamp: rawEvent.timestamp * 1000,_102 status: rawEvent.event,_102 messageId: rawEvent.sg_message_id.split('.')[0],_102 }))_102 // Ensure that events are sorted by time to ensure they're sent_102 // in the correct order_102 .sort((a, b) => a.timestamp - b.timestamp);_102_102 // Iterate over each event and wait for a text to be sent before_102 // processing the next event_102 for (const event of normalizedEvents) {_102 // You could also await an async operation to update your db records to_102 // reflect the status change here_102 // await db.updateEmailStatus(event.messageId, event.status, event.timestamp);_102 if (event.status === 'open') {_102 await sendSMSNotification(event.to, event.timestamp);_102 }_102 }_102_102 // Return a 200 OK!_102 return callback();_102};
sendgrid-email
Service
and add a
Public
/events/email
Function. Delete the default contents of the Function, and paste in the code snippet provided on this page.
Follow the instructions here to set up a SendGrid Event Webhook. Paste the URL of your newly created Function as the unique URL for the Event Webhook. (it will look like https://sendgrid-email-5877.twil.io/events/email
)