Receiving email-webhook Requests in AWS Lambda with SST

SST makes it easy to stand up an API Gateway v2 HTTP endpoint backed by a Lambda function. Once deployed, you paste the URL into the email-webhook dashboard and every incoming email lands in your function as a typed JSON payload.

This guide uses SST v3 (Ion), API Gateway v2, and TypeScript.

Prerequisites

  • An AWS account with credentials configured locally (aws configure or environment variables)
  • Node.js 18+
  • SST CLI installed (npm install -g sst)

Step 1: Create a new SST project

mkdir email-handler && cd email-handler
npx create-sst@latest

Choose the default Empty template when prompted. This creates:

email-handler/
โ”œโ”€โ”€ sst.config.ts
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ src/

Step 2: Define the API and function

Replace the contents of sst.config.ts:

/// <reference path="./.sst/platform/config.d.ts" />

export default $config({
  app(input) {
    return {
      name: "email-handler",
      // Non-production stages are torn down on `sst remove`; production resources are kept
      removal: input?.stage === "production" ? "retain" : "remove",
      home: "aws",
    };
  },
  async run() {
    // Stores the shared secret as an encrypted SST secret โ€” set it once with
    // `npx sst secret set WebhookSecret <value>` and it's available at runtime
    // via the Resource binding without any manual env var wiring
    const secret = new sst.Secret("WebhookSecret");

    // Creates an API Gateway v2 HTTP API (lower latency and cost than REST API)
    const api = new sst.aws.ApiGatewayV2("EmailApi");

    // Routes POST /webhook to the handler function; linking the secret
    // automatically grants the function permission to read it at runtime
    api.route("POST /webhook", {
      handler: "src/handler.handler",
      link: [secret],
    });

    return { url: api.url };
  },
});

Step 3: Write the handler

Create src/handler.ts:

import type {
  APIGatewayProxyEventV2,
  APIGatewayProxyResultV2,
} from "aws-lambda";
import { Resource } from "sst";

interface Attachment {
  filename: string;
  content: string; // base64-encoded
}

interface EmailPayload {
  from: string;
  to: string;
  subject: string;
  message: string;
  attachments: Attachment[];
}

export async function handler(
  event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> {
  // Verify the shared secret set in the email-webhook dashboard
  if (event.headers["x-my-api-key"] !== Resource.WebhookSecret.value) {
    return { statusCode: 401 };
  }

  // Use X-email-webhook-id as an idempotency key
  const deliveryId = event.headers["x-email-webhook-id"];

  const payload: EmailPayload = JSON.parse(event.body ?? "{}");

  console.log(JSON.stringify({
    deliveryId,
    from: payload.from,
    subject: payload.subject,
    attachments: payload.attachments.length,
  }));

  // Your logic here โ€” store to a database, trigger a workflow, etc.

  return { statusCode: 200 };
}

The handler does three things before any business logic:

  1. Authenticates the request using the X-My-Api-Key header.
  2. Reads the idempotency key (X-email-webhook-id) so you can detect duplicate deliveries.
  3. Parses the typed payload โ€” no any casts needed.

Step 4: Install types

npm install --save-dev @types/aws-lambda

Step 5: Set the secret and deploy

Set the secret value before the first deploy. Replace my-secret-token with a random string you generate (e.g. openssl rand -hex 32):

npx sst secret set WebhookSecret my-secret-token

Then deploy:

npx sst deploy --stage production

SST prints your endpoint URL when it finishes:

url: https://abc123.execute-api.us-east-1.amazonaws.com

Your webhook URL is:

https://abc123.execute-api.us-east-1.amazonaws.com/webhook

Step 6: Wire it up in the email-webhook dashboard

  1. Open the email-webhook dashboard and click New webhook (or edit an existing one).
  2. Set Webhook URL to your Lambda endpoint.
  3. Set HTTP Method to POST.
  4. Open the Custom Headers section and add:
Header name Header value
X-My-Api-Key my-secret-token
  1. Click Save.

Send a test email to your @email-webhook.com address. Within a few seconds your Lambda function receives the request and logs the delivery.


Handling attachments

Attachments arrive base64-encoded inside the attachments array. To upload them to S3, add an S3 bucket to your SST config and link it to the function:

// sst.config.ts (inside run())
const bucket = new sst.aws.Bucket("Attachments");

api.route("POST /webhook", {
  handler: "src/handler.handler",
  link: [secret, bucket],
});

return { url: api.url, bucket: bucket.name };

Then in the handler:

import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

const s3 = new S3Client({});

for (const attachment of payload.attachments) {
  const buffer = Buffer.from(attachment.content, "base64");
  await s3.send(new PutObjectCommand({
    Bucket: Resource.Attachments.name,
    Key: `${deliveryId}/${attachment.filename}`,
    Body: buffer,
  }));
}

SST automatically grants the Lambda function the necessary S3 permissions when you use link.


Staying within Lambda timeouts

API Gateway v2 has a hard 30-second integration timeout. If you have heavy processing to do (OCR, ML inference, large file uploads), acknowledge the request immediately and hand off to a queue:

import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";

const sqs = new SQSClient({});

export async function handler(event: APIGatewayProxyEventV2) {
  // auth check ...

  await sqs.send(new SendMessageCommand({
    QueueUrl: Resource.EmailQueue.url,
    MessageBody: event.body ?? "{}",
    MessageDeduplicationId: event.headers["x-email-webhook-id"],
    MessageGroupId: "email",
  }));

  return { statusCode: 200 };
}

Return 200 as soon as the message is enqueued. A second Lambda, subscribed to the queue, does the real work without any timeout pressure.


Next steps

  • Lock down your endpoint: see Authenticating Your Endpoint for all supported header patterns.
  • Debug deliveries: enable Message Logs to see HTTP status, duration, and payload metadata for each delivery.
  • Decode attachments: for deeper examples including type detection and safe filename handling, see Handling Attachments.