Skip to content

Webhook processing

When handling third-party webhooks (e.g. from Stripe, Shopify, or GitHub), you need to process them quickly to avoid timing out the sender’s server (usually required within 3-5 seconds). You must also handle duplicate events gracefully, as providers often guarantee at-least-once webhook delivery.

This example shows how to verify a Stripe webhook signature, immediately enqueue the payload using the Stripe eventId as an idempotencyKey, respond with HTTP 202 to the sender, and fulfill the order asynchronously in a consumer handler.

Terminal window
import { backend } from "@layeron/core"
import { queue } from "@layeron/modules"
const app = backend()
// 1. Declare a queue with a DLQ to handle persistent order failures
const stripeWebhookQueue = queue({
name: "stripe-webhooks",
retry: {
maxAttempts: 5,
backoff: "exponential",
},
deadLetter: {
name: "stripe-webhooks-dlq",
retentionDays: 14,
},
})
app.use(stripeWebhookQueue)
// 2. HTTP Webhook Ingress Route
app.post("/webhooks/stripe", async (request) => {
const signature = request.headers.get("stripe-signature")
if (!signature) {
return new Response("Missing signature", { status: 400 })
}
const rawBody = await request.text()
try {
// Validate the Stripe signature using your webhook secret
const event = await verifyStripeSignature(rawBody, signature, process.env.STRIPE_WEBHOOK_SECRET!)
// Use the unique Stripe Event ID as the queue idempotencyKey.
// If Stripe sends the exact same event ID twice, the second send is ignored.
const result = await stripeWebhookQueue.send(
{
type: event.type,
data: event.data.object,
},
{
idempotencyKey: event.id,
},
)
// Respond immediately with 202 Accepted.
// If the message was already sent and deduped, result.deduped will be true.
return Response.json({
received: true,
messageId: result.messageId,
deduped: !!result.deduped,
}, { status: 202 })
} catch (error) {
console.error("Webhook signature verification failed:", error)
return new Response("Invalid webhook signature", { status: 400 })
}
})
// 3. Asynchronous Consumer Handler
stripeWebhookQueue.consume(async (message) => {
const { type, data } = message.payload
if (type === "checkout.session.completed") {
const session = data as { id: string; client_reference_id: string; amount_total: number }
console.log(`[Queue] Processing completed checkout session: ${session.id}`)
// Execute business logic (e.g. provision licenses, send invoice, update database)
await fulfillCheckout(session.client_reference_id, session.amount_total)
}
})
// Helper mockup for signature verification
async function verifyStripeSignature(rawBody: string, signature: string, secret: string) {
// Stripe SDK or Web Crypto API validation goes here...
return {
id: "evt_1N3xyz...", // Unique event ID generated by Stripe
type: "checkout.session.completed",
data: {
object: {
id: "cs_test_abc",
client_reference_id: "user_999",
amount_total: 4900,
},
},
}
}
async function fulfillCheckout(userId: string, amount: number) {
// Database updates, provision entitlements...
}
  1. Gatekeeper Signature Check: The incoming webhook signature is verified first. If it is invalid, the request is rejected immediately before it touches the queue.
  2. Instant Delivery & Deduplication: The event is enqueued. Passing the Stripe Event ID event.id directly into the { idempotencyKey: event.id } parameter lets Layeron return the existing message instead of enqueueing a duplicate while the idempotency record is retained.
  3. Decoupled Fulfillments: The /webhooks/stripe endpoint returns a 202 Accepted status within milliseconds. The checkout fulfillment (e.g. updating Layeron Database records, calling mailing APIs) runs safely in the background consumer.