Access is invite-only during beta. Once invited, create a key to personalize these examples. Production sending requires a separate review.

Forward webhooks to Amazon EventBridge

Check the signature in Lambda, then call PutEvents.

This guide shows you how to put delivery webhooks on an Amazon EventBridge bus.

PostShiba POSTs a JSON array to an https URL. A Lambda function URL is that URL. The function checks the signature, then calls PutEvents. Rules on the bus route each event.

Create the bus

Create an event bus in the same Region as the function. The sample uses the name postshiba.

Allow PutEvents

Add this statement to the function's execution role. Replace the Region, account, and bus name.

policy.json
1 {
2 "Version": "2012-10-17",
3 "Statement": [
4 {
5 "Effect": "Allow",
6 "Action": "events:PutEvents",
7 "Resource": "arn:aws:events:REGION:ACCOUNT:event-bus/postshiba"
8 }
9 ]
10 }

Write the function

Use the Node.js 22 runtime. Install @aws-sdk/client-eventbridge in the function directory and deploy it with the handler.

Mark the package as a module so index.js can use import.

package.json
1 {
2 "type": "module"
3 }
install.sh
1 npm install @aws-sdk/client-eventbridge

Set POSTSHIBA_WEBHOOK_SECRET to the endpoint secret. Set EVENT_BUS_NAME to the bus name.

The signed string is {timestamp}.{raw body}. Check that string before parsing JSON. A HEAD with no signature is the liveness check. Return 204 for HEAD. A status of 500 or higher marks the endpoint down.

PutEvents accepts 10 entries per call. Return 2xx after every entry is accepted. Return 500 when FailedEntryCount is greater than zero so PostShiba retries. A retry sends the same body again. Use sg_event_id as the idempotency key. See Retries and replays.

Return within 10 seconds.

index.js
1 import { createHmac, timingSafeEqual } from "node:crypto"
2 import { EventBridgeClient, PutEventsCommand } from "@aws-sdk/client-eventbridge"
3
4 const client = new EventBridgeClient({})
5 const secret = process.env.POSTSHIBA_WEBHOOK_SECRET
6 const busName = process.env.EVENT_BUS_NAME
7
8 export const handler = async (event) => {
9 if (event.requestContext?.http?.method === "HEAD") {
10 return { statusCode: 204, body: "" }
11 }
12
13 const raw = event.isBase64Encoded
14 ? Buffer.from(event.body || "", "base64").toString("utf8")
15 : event.body || ""
16 const headers = event.headers || {}
17
18 if (!verify(raw, headers["x-capsule-timestamp"], headers["x-capsule-signature"])) {
19 return { statusCode: 401, body: "invalid signature" }
20 }
21
22 let batch
23 try {
24 batch = JSON.parse(raw)
25 } catch {
26 return { statusCode: 400, body: "invalid json" }
27 }
28 if (!Array.isArray(batch)) {
29 return { statusCode: 400, body: "expected an array" }
30 }
31
32 for (let i = 0; i < batch.length; i += 10) {
33 const result = await client.send(
34 new PutEventsCommand({
35 Entries: batch.slice(i, i + 10).map((item) => ({
36 EventBusName: busName,
37 Source: "postshiba",
38 DetailType: String(item.event || "event"),
39 Detail: JSON.stringify(item),
40 })),
41 }),
42 )
43 if (result.FailedEntryCount) {
44 return { statusCode: 500, body: "put events failed" }
45 }
46 }
47
48 return { statusCode: 204, body: "" }
49 }
50
51 function verify(raw, timestamp, signature) {
52 if (!secret || !timestamp || !signature) return false
53 const expected = createHmac("sha256", secret).update(`${timestamp}.${raw}`).digest("hex")
54 const given = String(signature).replace(/^sha256=/, "")
55 const a = Buffer.from(expected)
56 const b = Buffer.from(given)
57 if (a.length !== b.length) return false
58 return timingSafeEqual(a, b)
59 }

Open a function URL

Create a function URL with auth type NONE, payload format 2.0, and buffered invoke mode. The function returns 401 when the signature check fails.

Copy the https:// URL. Production webhook URLs must be https.

Create the webhook

  1. Create an endpoint with that URL in the dashboard or with the API. See Managing webhooks.
  2. Copy secret from the create response into POSTSHIBA_WEBHOOK_SECRET.
  3. Enable the endpoint and pick event types. See Event types.

Add a rule

On the postshiba bus, match source postshiba. Set the detail type to the event name, such as delivered or bounce.

Detail is one object from the array. Fields are listed on Event types.

Inbox webhooks use the same headers with webhook_secret. See Receiving webhooks.

About

PostShiba is the email platform that powers Bento behind the scenes. You can build your own products, like Bento, on top of it.

© 2026 PostShiba by Backpack Internet Pty. Ltd. All rights reserved.

The same policies that govern Bento are applied to PostShiba Privacy | Terms | Security