Skip to contentSkip to navigationSkip to topbar
On this page

Validate Webhook requests from SendGrid


(warning)

Warning

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(link takes you to an external page), 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.


Create and host a Function

create-and-host-a-function page anchor

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:

ConsoleServerless Toolkit

If you prefer a UI-driven approach, creating and deploying a Function can be done entirely using the Twilio Console and the following steps:

  1. Log in to the Twilio Console and navigate to the Functions tab(link takes you to an external page). If you need an account, you can sign up for a free Twilio account here(link takes you to an external page)!
  2. Functions are contained within Services. Create a Service by clicking the Create Service(link takes you to an external page) button and providing a name such as test-function.
  3. Once you've been redirected to the new Service, click the Add + button and select Add Function from the dropdown.
  4. This will create a new Protected Function for you with the option to rename it. The name of the file will be path it is accessed from.
  5. Copy any one of the example code snippets from this page that you want to experiment with, and paste the code into your newly created Function. You can quickly switch examples by using the dropdown menu of the code rail.
  6. Click Save to save your Function's contents.
  7. Click Deploy All to build and deploy the Function. After a short delay, your Function will be accessible from: https://<service-name>-<random-characters>-<optional-domain-suffix>.twil.io/<function-path>
    For example: 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!

Validate Webhook requests from SendGrid

validate-webhook-requests-from-sendgrid page anchor
1
const { EventWebhook, EventWebhookHeader } = require('@sendgrid/eventwebhook');
2
3
// Helper method for validating SendGrid requests
4
const verifyRequest = (publicKey, payload, signature, timestamp) => {
5
// Initialize a new SendGrid EventWebhook to expose helpful request
6
// validation methods
7
const eventWebhook = new EventWebhook();
8
// Convert the public key string into an ECPublicKey
9
const ecPublicKey = eventWebhook.convertPublicKeyToECDSA(publicKey);
10
return eventWebhook.verifySignature(
11
ecPublicKey,
12
payload,
13
signature,
14
timestamp
15
);
16
};
17
18
exports.handler = async (context, event, callback) => {
19
// Access a pre-initialized Twilio client from context
20
const twilioClient = context.getTwilioClient();
21
// Access sensitive values such as the sendgrid key and phone numbers
22
// from Environment Variables
23
const publicKey = context.SENDGRID_WEBHOOK_PUBLIC_KEY;
24
const twilioPhoneNumber = context.TWILIO_PHONE_NUMBER;
25
const numberToNotify = context.NOTIFY_PHONE_NUMBER;
26
27
// The SendGrid EventWebhookHeader provides methods for getting
28
// the necessary header names.
29
// Remember to cast these header names to lowercase to access them correctly
30
const signatureKey = EventWebhookHeader.SIGNATURE().toLowerCase();
31
const timestampKey = EventWebhookHeader.TIMESTAMP().toLowerCase();
32
33
// Retrieve SendGrid's headers so they can be used to validate
34
// the request
35
const signature = event.request.headers[signatureKey];
36
const timestamp = event.request.headers[timestampKey];
37
38
// Runtime injects the request object and spreads in the SendGrid events.
39
// Isolate the original SendGrid event contents using destructuring
40
// and the rest operator
41
const { request, ...sendGridEvents } = event;
42
// Convert the SendGrid event back into an array of events, which is the
43
// format sent by SendGrid initially
44
const sendGridPayload = Object.values(sendGridEvents);
45
46
// Stringify the event and add newlines/carriage returns since they're expected by validator
47
const rawEvent =
48
JSON.stringify(sendGridPayload).split('},{').join('},\r\n{') + '\r\n';
49
50
// Verify the request using the public key, the body of the request,
51
// and the SendGrid headers
52
const valid = verifyRequest(publicKey, rawEvent, signature, timestamp);
53
// Reject invalidated requests!
54
if (!valid) return callback("Request didn't come from SendGrid", event);
55
56
// Helper method to simplify repeated calls to send messages with
57
// nicely formatted timestamps
58
const sendSMSNotification = (recipientEmail, timestamp) => {
59
const formattedDateTime = new Intl.DateTimeFormat('en-US', {
60
year: 'numeric',
61
month: 'numeric',
62
day: 'numeric',
63
hour: 'numeric',
64
minute: 'numeric',
65
second: 'numeric',
66
hour12: true,
67
timeZone: 'America/Los_Angeles',
68
}).format(timestamp);
69
70
return twilioClient.messages.create({
71
from: twilioPhoneNumber,
72
to: numberToNotify,
73
body: `Email to ${recipientEmail} was opened on ${formattedDateTime}.`,
74
});
75
};
76
77
// Convert the original list of events into a condensed version for SMS
78
const normalizedEvents = sendGridPayload
79
.map((rawEvent) => ({
80
to: rawEvent.email,
81
timestamp: rawEvent.timestamp * 1000,
82
status: rawEvent.event,
83
messageId: rawEvent.sg_message_id.split('.')[0],
84
}))
85
// Ensure that events are sorted by time to ensure they're sent
86
// in the correct order
87
.sort((a, b) => a.timestamp - b.timestamp);
88
89
// Iterate over each event and wait for a text to be sent before
90
// processing the next event
91
for (const event of normalizedEvents) {
92
// You could also await an async operation to update your db records to
93
// reflect the status change here
94
// await db.updateEmailStatus(event.messageId, event.status, event.timestamp);
95
if (event.status === 'open') {
96
await sendSMSNotification(event.to, event.timestamp);
97
}
98
}
99
100
// Return a 200 OK!
101
return callback();
102
};

Create your Function and connect it to SendGrid

create-your-function-and-connect-it-to-sendgrid page anchor
  1. First, create a new 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.

  2. Create a free SendGrid account(link takes you to an external page).

  3. Follow the instructions here(link takes you to an external page) 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)

    SendGrid event webhook URL.
  4. Follow these steps(link takes you to an external page) to enable the Signed Event Webhook Requests. This will add signed SendGrid headers to incoming webhook requests, which we can then use to validate requests!

    Enable SendGrid Signed Event Webhook Requests.
  5. Copy the generated Verification Key from the last step, and save it as an Environment variable in Runtime as SENDGRID_WEBHOOK_PUBLIC_KEY. While here, also save your TWILIO_PHONE_NUMBER (from the Twilio console) and a NOTIFY_PHONE_NUMBER (this could be your personal phone number for now)

    Set SendGrid verification key as an environment variable.
  6. Add the @sendgrid/eventwebhook dependency as *, and ensure that the @twilio/runtime-handler dependency is set to version 1.3.0 or later to enable headers.

    sendgrid webhook dependency.
  7. Save your Function and deploy it by clicking on Deploy All.

    sendgrid email deploy all.

Now that you've deployed your Function, it's time to validate that your code and its integration with SendGrid is working properly. In order to do so, you'll need to generate some email events. This will be accomplished with a short script written in JavaScript and using the @sendgrid/mail library.

Setup environment variables

setup-environment-variables page anchor

First, grab your SendGrid API Key(link takes you to an external page) (or create one!). For security, we'll be setting it as an environment variable and using it in our code instead of directly hard-coding it. You can do so by performing the following commands in your terminal, making sure to replace the YOUR_API_KEY placeholder with your own key.

1
echo "export SENDGRID_API_KEY='YOUR_API_KEY'" > sendgrid.env
2
echo "sendgrid.env" >> .gitignore
3
source ./sendgrid.env

Install the helper library

install-the-helper-library page anchor

Next, use npm(link takes you to an external page) or yarn to install the Node.js SendGrid helper library which will enable you to send emails using JavaScript. If you already have Node.js(link takes you to an external page) installed, it's very likely you already have npm available and ready to use.

npm install --save @sendgrid/mail

If you prefer yarn(link takes you to an external page) instead:

yarn add @sendgrid/mail

Add and verify a Sender Identity

add-and-verify-a-sender-identity page anchor

Before you can successfully send an email, you'll need to verify an email address or domain in the Sender Authentication tab(link takes you to an external page). Without this, you will receive a 403 Forbidden response when attempting to send mail.

Once you have prepared your environment variables, installed the SendGrid helper library, and your email has been validated, you're ready to send some emails and create some events.

Create a new file such as send-email.js, and paste in the following snippet. Be sure to replace the from variable with your verified email address from earlier, as well as the to variable (in this case, you can use your verified email address here as well). Save the file.

1
// Using Twilio SendGrid's v3 Node.js Library
2
// https://github.com/sendgrid/sendgrid-nodejs
3
const sgMail = require('@sendgrid/mail');
4
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
5
6
sgMail
7
.send({
8
from: 'test@example.com', // Change to your verified sender
9
to: 'test@example.com', // Change to your recipient
10
subject: 'Sending with Twilio SendGrid is Fun',
11
text: 'and easy to do anywhere, even with Node.js',
12
html: '<strong>and easy to do anywhere, even with Node.js</strong>',
13
})
14
.then(() => {
15
console.log('Email sent');
16
})
17
.catch((error) => {
18
console.error(error);
19
});

Once the script is saved, you can send your test email by executing the script with Node.js:

node send-email.js

Once you've received your email and opened it, you should receive a text message from your Function a short time later!

If you would like to expedite this process a bit and not wait for the open event itself, you could modify line 95 of the Function body to instead check for delivered events instead. A delivered event will be emitted and processed by your Function almost immediately after executing the send-email script.

Need some help?

Terms of service

Copyright © 2024 Twilio Inc.