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.
Code Implementation
Section titled “Code Implementation”import { backend } from "@layeron/core"import { queue } from "@layeron/modules"
const app = backend()
// 1. Declare a queue with a DLQ to handle persistent order failuresconst stripeWebhookQueue = queue({ name: "stripe-webhooks", retry: { maxAttempts: 5, backoff: "exponential", }, deadLetter: { name: "stripe-webhooks-dlq", retentionDays: 14, },})
app.use(stripeWebhookQueue)
// 2. HTTP Webhook Ingress Routeapp.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 HandlerstripeWebhookQueue.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 verificationasync 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...}How It Works
Section titled “How It Works”- Gatekeeper Signature Check: The incoming webhook signature is verified first. If it is invalid, the request is rejected immediately before it touches the queue.
- Instant Delivery & Deduplication: The event is enqueued. Passing the Stripe Event ID
event.iddirectly into the{ idempotencyKey: event.id }parameter lets Layeron return the existing message instead of enqueueing a duplicate while the idempotency record is retained. - Decoupled Fulfillments: The
/webhooks/stripeendpoint returns a202 Acceptedstatus within milliseconds. The checkout fulfillment (e.g. updating Layeron Database records, calling mailing APIs) runs safely in the background consumer.