hooksentinel
Frameworks

AWS Lambda

Verify webhooks in an AWS Lambda function behind API Gateway, including the base64 body encoding gotcha.

Install

npm install @hooksentinel/core

@hooksentinel/core/lambda exports toLambdaHandler, built for API Gateway (REST and HTTP APIs) and Lambda Function URLs.

Basic setup

src/handler.ts
import { createWebhookHandler, stripe } from '@hooksentinel/core';
import { toLambdaHandler } from '@hooksentinel/core/lambda';
import { redisStore } from '@hooksentinel/core/stores';

const stripeWebhook = createWebhookHandler({
  provider: stripe({ secret: process.env.STRIPE_WEBHOOK_SECRET! }),
  idempotency: redisStore({ url: process.env.REDIS_URL! }),
  onEvent: async (event) => {
    if (event.type === 'checkout.session.completed') {
      await fulfillOrder(event.data.object.id);
    }
  },
});

export const handler = toLambdaHandler(stripeWebhook);

async function fulfillOrder(sessionId: string) {
  // ...
}

toLambdaHandler returns a standard APIGatewayProxyHandlerV2 (also compatible with V1 REST API events) — point API Gateway's integration directly at it.

The base64 body encoding gotcha

API Gateway may deliver the request body base64-encoded rather than as a plain string, depending on the Content-Type and your API Gateway configuration — this is controlled by isBase64Encoded on the event object. If your handler reads event.body directly as a UTF-8 string when it's actually base64, the bytes you verify the signature against won't match what the provider signed, and every request fails with invalid_signature.

toLambdaHandler checks event.isBase64Encoded and decodes correctly either way — you don't need to handle this yourself as long as you go through the adapter rather than reading event.body in your own code before calling it.

If you see intermittent invalid_signature errors that don't reproduce locally, confirm your API Gateway integration is configured to pass through binary media types correctly for application/json, since a misconfigured binaryMediaTypes list on a REST API can cause API Gateway itself to mangle the body before Lambda ever sees isBase64Encoded.

Idempotency: Redis

Lambda has no persistent memory between invocations, so memoryStore() is not usable — each cold or concurrent invocation starts with an empty Map. Point redisStore() at a Redis instance reachable from your VPC (ElastiCache) or over HTTP (Upstash, which works well from Lambda without VPC networking overhead):

import { redisStore } from '@hooksentinel/core/stores';

redisStore({
  url: process.env.REDIS_URL!,
  ttlSeconds: 86_400, // optional — defaults to 24h
});

See Idempotency: Redis store for the full store setup, including the ioredis client option if you'd rather pass in an existing connection than a URL.

Cold starts and fast-ack

API Gateway enforces a 29-second integration timeout regardless of your Lambda's own timeout setting. If onEvent does slow work, either keep the Lambda's own timeout well under 29s so you get a clean failure instead of a gateway timeout, or hand off to SQS and ack immediately — the same pattern described in Idempotency: the BullMQ fast-ack pattern applies with SQS in place of BullMQ.

Function URLs

Function URLs use the same event shape as API Gateway HTTP APIs (payload format 2.0), so toLambdaHandler works without changes — just point the Function URL at the same exported handler.

Last updated on

On this page