Expect more than one delivery

A payment webhook cannot be assumed to arrive once, in order and without network errors. Responses can time out and the sender may retry. If every delivery triggers fulfillment, one event can create repeated business effects. We use Stripe’s documentation for a concrete example, but reliable receipt is a general integration concern. Design repetition and interruption into the normal flow instead of treating them as surprising exceptions.

Authenticate the message before taking action. Stripe signature verification uses the raw body and the specific endpoint secret. Parsing JSON and reconstructing text first can break verification. Separate test and production secrets and read the original signature header. HTTPS and signature checks play distinct roles. Our teaching endpoint validates the message and stores its ID in an inbox. Receipt is not proof that a payment is successful or that fulfillment is authorized.

Separate receipt from business work

Keep HTTP handling short: verify, persist and return the appropriate response. Move slow work to a worker. Responding successfully before durable storage can lose an event if the process then crashes. Performing all business work inline increases timeout and retry risk. The sample uses SQLite only to demonstrate a small persistent inbox. Worker scheduling, retries and the production database require a separate design.

Use a database uniqueness constraint on event IDs so concurrent duplicate deliveries cannot create two records. A check-then-insert without database enforcement is race-prone. An event ID is not a complete business idempotency strategy: distinct events may concern the same operation. Final processing also needs an operation key, record state and transactional checks. Idempotency means that repeated execution has no additional effect, not simply that the second log entry looks different.

A worker should claim pending messages, validate business rules and record a clear result. Marking completion before an external effect risks losing work; marking it afterward can cause repetition after a crash. Transactions, outbox patterns and destination idempotency keys help address that gap. Store only the payment and customer information required and define retention and access policies for the inbox instead of treating it as an unrestricted archive.

Code example and verification

This educational example demonstrates the implementation path. Check the stated runtime and prerequisites in a test environment; the notes explain what remains before production use.

FastAPI; a durable inbox demo only
# python -m pip install fastapi uvicorn stripe
# Configure STRIPE_WEBHOOK_SECRET, then: uvicorn inbox_demo:app
import os, sqlite3
from contextlib import closing
import stripe
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
secret = os.environ["STRIPE_WEBHOOK_SECRET"]
with closing(sqlite3.connect("webhook-inbox.db")) as db, db:
    db.execute("""CREATE TABLE IF NOT EXISTS inbox (
      event_id TEXT PRIMARY KEY, event_type TEXT NOT NULL,
      payload BLOB NOT NULL, status TEXT NOT NULL DEFAULT 'pending'
    )""")

@app.post("/webhook")
async def webhook(request: Request):
    payload = await request.body()
    signature = request.headers.get("stripe-signature", "")
    try:
        event = stripe.Webhook.construct_event(payload, signature, secret)
    except (ValueError, stripe.SignatureVerificationError):
        raise HTTPException(400, "Invalid webhook")
    try:
        with closing(sqlite3.connect("webhook-inbox.db", timeout=5)) as db, db:
            db.execute("""INSERT INTO inbox(event_id,event_type,payload)
              VALUES (?,?,?) ON CONFLICT(event_id) DO NOTHING""",
              (event["id"],event["type"],payload))
    except sqlite3.Error:
        raise HTTPException(503, "Inbox unavailable")
    return {"received": True}

Save as inbox_demo.py and configure the test endpoint secret. Repeated signed delivery should create one pending row; invalid signatures return 400 and storage failures return 503. No order is fulfilled. Add a worker, event-type handling, amount/state checks, request-size limits and secure payload retention. Synchronous SQLite inside this teaching handler is not a production concurrency design.

Make retries and crashes part of the test

Test valid input, invalid signatures, repeated IDs and process termination after persistence. One pending record should remain available to resume. Then test out-of-order events and failed external effects. Monitor accepted events, processing delay, retries and manual interventions. Reliability means a path that can continue after failure. Returning success for everything or retaining IDs in a process-local set does not meet that requirement.

Implementation checklist

  • Verify the original raw body before parsing or reconstructing it.
  • Read endpoint secrets from configuration and separate test from production.
  • Persist IDs using a database uniqueness constraint.
  • Separate successful receipt from completed business work.

Practical explanations and recommendations are Liyan Knowledge editorial analysis.Sources: Stripe — Webhooks · Stripe — Verify webhook signatures · Stripe — Idempotent requests

This Liyan Knowledge article is an editorial synthesis based on the original source.View original source