Inquir Compute logoInquir Compute
Use case · Stripe

Process Stripe webhooks serverlessly with retries and logs

Stripe retries webhooks for up to 72 hours on failure. Handle them correctly: verify the HMAC signature on the raw body, write an idempotency key before any mutation, acknowledge within 30 seconds, and continue fulfillment in a background pipeline.

Last updated: 2026-04-20

Direct answer

Process Stripe webhooks serverlessly with retries and logs. One function for Stripe ingress: verify signature on raw body, write event ID as idempotency key, return 200 fast, trigger fulfillment pipeline.

When it fits

  • payment_intent.succeeded, charge.refunded, subscription.updated, invoice.paid
  • Any Stripe event that triggers fulfillment, email, or inventory changes

Tradeoffs

  • Mixing user auth, payment webhook, and fulfillment in one endpoint makes the stripe-signature header logic fragile and couples scaling dimensions you want separate.
  • Inline fulfillment (charge → fulfill → email in one request) means Stripe retry pressure hits your slowest operation.

The three Stripe webhook failure modes

  • Parsing body before HMAC verification: signature mismatch on valid events
  • Slow inline fulfillment: Stripe retries after 30s, causing double-charges
  • Missing idempotency key: retry delivers event twice, two orders created

Stripe signs webhooks with HMAC-SHA256 over the raw body. If you JSON.parse before verifying, whitespace normalization can break the signature. If your handler runs fulfillment synchronously and takes over 30 seconds, Stripe retries—and without idempotency, that means duplicate side effects.

Why monolithic API handlers struggle with Stripe

Mixing user auth, payment webhook, and fulfillment in one endpoint makes the stripe-signature header logic fragile and couples scaling dimensions you want separate.

Inline fulfillment (charge → fulfill → email in one request) means Stripe retry pressure hits your slowest operation.

Stripe webhook pattern on Inquir

One function for Stripe ingress: verify signature on raw body, write event ID as idempotency key, return 200 fast, trigger fulfillment pipeline.

The fulfillment pipeline runs outside the HTTP window—no Stripe timeout pressure. Retries apply per step; execution history shows every event processed.

Stripe webhook implementation checklist

Raw body for HMAC

event.body arrives as a string—never JSON.parse before stripe.webhooks.verifySignature.

Idempotency key

Upsert evt.id before any mutation. If the upsert finds an existing record, skip and return 200.

Fast ACK

Return 200 before starting fulfillment. Stripe expects response within 30 seconds.

Pipeline for fulfillment

Trigger async pipeline for order fulfillment, email, and inventory updates after ACK.

Stripe webhook flow

1

Verify HMAC on raw body

Read event.body as string. Call stripe.webhooks.verifySignature with raw body and stripe-signature header.

2

Write idempotency key, return 200

Upsert evt.id in your events table. Return immediately—do not start fulfillment yet.

3

Trigger fulfillment orchestration

Call global.durable.startNew with event type and data. The orchestration handles retries, email, and inventory updates independently.

Stripe webhook handler

Complete pattern: raw body, signature verify, idempotency key, fast ACK, async fulfillment pipeline.

webhooks/stripe.mjs
export async function handler(event) {
  const rawBody = event.body ?? '';
  const sig = event.headers['stripe-signature'] ?? '';
  let evt;
  try {
    evt = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    return { statusCode: 400, body: `Webhook Error: ${err.message}` };
  }
  const isNew = await db.webhookEvents.upsert({ id: evt.id, type: evt.type });
  if (!isNew) return { statusCode: 200, body: 'duplicate' };
  if (evt.type === 'payment_intent.succeeded') {
    await global.durable.startNew('stripe-fulfill', undefined, { intentId: evt.data.object.id, amount: evt.data.object.amount });
  }
  return { statusCode: 200, body: JSON.stringify({ received: true }) };
}

Use this pattern for

When this works

  • payment_intent.succeeded, charge.refunded, subscription.updated, invoice.paid
  • Any Stripe event that triggers fulfillment, email, or inventory changes

When to skip it

  • Read-only Stripe events where duplicate delivery has no side effects

FAQ

Which Stripe events should I handle?

payment_intent.succeeded for purchase fulfillment; invoice.payment_failed for subscription dunning; customer.subscription.deleted for access revocation.

How do I test without real Stripe traffic?

Use stripe listen --forward-to <local URL> for local testing, or stripe trigger payment_intent.succeeded to send a test event to your deployed function.

Inquir Compute logoInquir Compute

The simplest way to run AI agents and backend jobs without infrastructure.

Contact info@inquir.org

© 2025 Inquir Compute. All rights reserved.