Serverless Stripe Webhooks on AWS with Lambda Function URLs
Serverless Typescript
We will start with aws-nodejs-typescript
template. And we need to customize the default template to support local development and debugging.
Clone Typescript Template
Let’s start with the latest serverless version
npm install -g serverless
sls create --template aws-nodejs-typescript --path stripe-webhooks
cd stripe-webhooks
npm ci
stripe-webhooks
is project directory.
Open the project with your favorite IDE.
serverless-offline
We will install serverless-offline
and make sure that we can trigger a function via an HTTP method
npm install serverless-offline -D
then update plugins
path of serverless.ts
to add the new plugin to the plugin list:
plugins: [
'serverless-esbuild',
'serverless-offline',
],
You can custom serverless offline command option by adding serverless-offline
property to serverless.ts
‘s custom
:
custom: {
esbuild: {
bundle: true,
minify: false,
sourcemap: true,
exclude: [
'aws-sdk',
'@aws-lambda-powertools/logger',
],
target: 'node14',
define: { 'require.resolve': undefined },
platform: 'node',
concurrency: 10,
watch: {
pattern: ['src/**/*.ts'],
ignore: ['.build', 'dist', 'node_modules', '.serverless'],
},
} as EsbuildOptions,
'serverless-offline': {
noPrependStageInUrl: true,
reloadHandler: true,
},
},
watch
watch options for serverless-offline
, we want to rebuild the project when we save any ts file.
reloadHandle
Reloads handler with each request. We need to enable this option to make watch
option make sense.
You can add a script to package.json to create a shortcut to start offline mode:
"scripts": {
"start": "sls offline"
},
From now, we can start local server with the start
command
npm start
Let’s verify our setup by calling POST /hello
with a json object as a request body:
curl -X POST --location "http://localhost:3000/hello" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"Foo\"
}"
aws-lambda-powertools/logger
Powertools is a developer toolkit to implement Serverless best practices and increase developer velocity.
In our project, we will integrate logger
util. You can read more about logger
at this official link.
In general, we have to install `@aws-lambda-powertools/logger` package, config logging, and inject the logger to middy middleware.
npm install @aws-lambda-powertools/logger -D
We install the package as a dev dependency because AWS provides Lambda Powertools Lambda Layer and you can inject the layer into your project by using their Layer ARN: https://awslabs.github.io/aws-lambda-powertools-typescript/latest/#lambda-layer.
This means we will update serverless.ts to add a layer to our functions and exclude the new package.
Add layers
to provider
block (my aws region is ap-northest-1
):
layers: [
'arn:aws:lambda:ap-northeast-1:094274105915:layer:AWSLambdaPowertoolsTypeScript:10',
],
and add `@aws-lambda-powertools/logger` to custom.esbuild.exclude
:
exclude: [
'aws-sdk',
'@aws-lambda-powertools/logger',
],
Now, let’s config the logger, in this story, I will config the logger by setting environment variables. Add these env to provider.environment
of serverless.ts
LOG_LEVEL: 'INFO',
POWERTOOLS_SERVICE_NAME: 'stripe-webhooks',
POWERTOOLS_LOGGER_LOG_EVENT: 'true',
NOTE: `POWERTOOLS_LOGGER_LOG_EVENT` will log all incoming events, just consider turning this option off when your event may include sensitive data.
The last step is injecting the logger into middy. I will create logger.ts
at libs
directory.
import { Logger } from '@aws-lambda-powertools/logger';
export default new Logger();
and update libs/lambda.ts
:
import middy from '@middy/core';
import middyJsonBodyParser from '@middy/http-json-body-parser';
import { injectLambdaContext } from '@aws-lambda-powertools/logger';
import logger from '@libs/logger';
export const middyfy = (handler) => {
return middy(handler).use([
injectLambdaContext(logger),
middyJsonBodyParser(),
]);
};
Now, when you call the hello
api, you can see a log will be printed out that includes your request information.
Stripe webhooks with Lambda Function URL
Webhooks lambda function
Make sure that you have enough knowledge about webhook, nodejs, or Stripe Webhooks.
In this part, we will create a lambda function to handle Stripe webhook event. Our lambda function is a lambda function URL, which means we can trigger the function via a url without AWS API Gateway.
To work with Stripe webhooks we need Stripe's Secret key and Webhook signing secret.
You can get the secret key on your Stripe developer page. And the signing, you can get it when you run stripe listen
command.
Make sure that you already install Stripe CLI.
Let’s build a simple handle function to handle Stripe webhook events, we can follow this official document
First, we need to install Stripe api node package:
npm install stripe
add a new handle function at src/functions/webhooks/handler.ts
import { middyfy } from '@libs/lambda';
import Stripe from 'stripe';
import * as process from 'process';
import { APIGatewayProxyHandler } from 'aws-lambda';
import logger from '@libs/logger';
const webhooks: APIGatewayProxyHandler = async (event) => {
try {
const endpointSecret = process.env.STRIPE_WEBHOOK_SIGNING_SECRET;
const signature = event.headers['Stripe-Signature'];
const stripe = new Stripe(
process.env.STRIPE_SECRET_KEY,
{
apiVersion: '2022-11-15',
},
);
const stripeEvent = stripe.webhooks.constructEvent(
event.body,
signature,
endpointSecret,
);
if (stripeEvent.type === 'payment_intent.succeeded') {
const paymentIntent = stripeEvent.data.object as Stripe.PaymentIntent;
logger.info(`PaymentIntent for ${paymentIntent.amount} was successful!`);
}
return {
body: '',
statusCode: 200,
};
} catch (error) {
logger.error(`⚠️ Webhook signature verification failed: ${error.message}`);
return {
body: '',
statusCode: 400,
};
}
};
export const main = middyfy(webhooks);
The handle type is `APIGatewayProxyHandler` instead of `ValidatedEventAPIGatewayProxyEvent` because in this example we want to parse the request body by stripe.webhooks.constructEvent
function, then the body should be a JSON string.
We get the request signature from the request header by Stripe-Signature
key, not stripe-signature
.
And this function requires 2 environment variables: STRIPE_SECRET_KEY
, STRIPE_SIGNING_SECRET
.
Now, we have to config the webhook lambda function. We will define the function as a lambda function URL.
src/functions/webhooks/index.ts
import { handlerPath } from '@libs/handler-resolver';
import type { AWS } from '@serverless/typescript';
import * as process from 'process';
export default {
handler: `${handlerPath(__dirname)}/handler.main`,
url: true,
...(
process.env.IS_OFFLINE
? {
events: [
{
http: {
method: 'post',
path: 'webhooks',
},
},
],
}
: {}
),
} as AWS['functions'][0];
Serverless-offline library does not support URL mode yet, then there is a trick to make the function can be tested locally, we define an HTTP event to trigger the function. The IS_OFFLINE
environment variable should be defined at compile time.
Don’t forget to export the function in the functions index file.
export { default as webhooks } from './webhooks';
As I mentioned before, we don't need to parse the event body by ourselves, then we will reject `middyJsonBodyParser` from middy:
// src/libs/lambda.ts
import middy from '@middy/core';
import { injectLambdaContext } from '@aws-lambda-powertools/logger';
import logger from '@libs/logger';
export const middyfy = (handler) => {
return middy(handler).use([
injectLambdaContext(logger),
]);
};
Go to serverless.ts
file, and update environments
and functions
parts:
import { webhooks } from '@functions/index';
// ...
environment: {
AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
NODE_OPTIONS: '--enable-source-maps --stack-trace-limit=1000',
LOG_LEVEL: 'INFO',
POWERTOOLS_SERVICE_NAME: 'stripe-webhooks',
POWERTOOLS_LOGGER_LOG_EVENT: 'false',
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SIGNING_SECRET: process.env.STRIPE_WEBHOOK_SIGNING_SECRET,
},
// ...
functions: {
webhooks,
},
// ...
We have to provide some env, so I choose dotenv to control them on the local side:
npm install dotenv -D
and this is .env
template:
IS_OFFLINE=true
STRIPE_SECRET_KEY=sk_live_YOUR_SECRET_KEY
STRIPE_WEBHOOK_SIGNING_SECRET=whsec_STRIPE_WEBHOOK_SIGNING_SECRET
Update package json’s start command to preload dotenv:
"scripts": {
"start": "node -r dotenv/config node_modules/.bin/sls offline"
},
Verify the webhooks function
In a new terminal window, let's start forwarding the stripe webhooks to your local URL:
stripe listen --forward-to http://localhost:3000/webhooks
the output will be look like this (let’s keep this process):
Ready! Your webhook signing secret is '{{WEBHOOK_SIGNING_SECRET}}' (^C to quit)
let’s copy and update `WEBHOOK_SIGNING_SECRET` value to your .env
file.
After updating the .env
file, you can start the local server:
npm start
In another terminal, trigger Stripe’s events to test your webhook:
stripe trigger payment_intent.succeeded
and you can get some log in the server terminal:
{
"cold_start":true,
"function_arn":"offline_invokedFunctionArn_for_stripe-webhooks-dev-webhooks",
"function_memory_size":1024,
"function_name":"stripe-webhooks-dev-webhooks",
"function_request_id":"d9e38439-68b5-4475-b554-3e6d3a625eb7",
"level":"INFO",
"message":"PaymentIntent for 2000 was successful!",
"service":"stripe-webhooks","timestamp":"2023-04-10T04:11:05.650Z"
}
(λ: webhooks) RequestId: d9e38439-68b5-4475-b554-3e6d3a625eb7 Duration: 469.25 ms Billed Duration: 470 ms
It works!
Deploy the function to AWS Lambda
Just try to complete your function and you can make it go live by deploying the function to AWS.
After you have set up your AWS access and environment variables, you can deploy your webhook with this command:
npx sls deploy
The command may take a few minutes to create a Lambda function and deploy it to AWS. Once the deployment completes, you will see the public URL that was assigned to the deployed webhook:
✔ Service deployed to stack stripe-webhooks-acp (556s)
endpoint: https://xxxxxxxxxxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/
functions:
webhooks: stripe-webhooks-acp-webhooks (51 MB)
Copy the URLs shown in the endpoint
line, this is the URL assigned to your webhook.
And you can follow this link to register the endpoint on the Stripe dashboard so Stripe knows where to deliver events.
Conclusion
The utilization of the Serverless framework for deploying webhooks to AWS Lambda in production is a highly convenient option, which has been made even more robust with the introduction of Lambda’s latest function URLs. I trust that you have found this tutorial to be beneficial and that you will utilize the methods that you have learned for your own webhooks.
You can find the complete source code at this GitHub link.
Thank you for reading!