A successful HTTP response from a Rails webhook tells a sender that the endpoint accepted its event. It does not tell your support team whether the associated work finished. If the handler acknowledges an event while its only remaining copy sits in a volatile queue, losing that queue can leave nothing to retry locally.
A useful acceptance rule is to acknowledge after the application has durably recorded enough trusted information to recover the work. Processing can then happen separately. The record needs an owner, a recovery path and an observable unfinished state. A table that nobody revisits is not a recovery mechanism.
Separate Rails webhook acceptance from completion
Stripe’s webhook documentation recommends a prompt successful response before complex processing, documents duplicate deliveries and does not guarantee event order. Manual resends can overlap automatic retries. Those properties make a receiver’s acceptance decision distinct from the business operation it starts.
For an actual Stripe endpoint, verify the signature against the unmodified request body using the supported integration path. Restrict accepted event types and versions, and establish the account context from trusted configuration. Do this before treating a payload as work. Do not use an event’s creation timestamp as a general ordering or deduplication key.
The application also needs to distinguish a receipt identifier from the resource being updated. Two different event IDs may refer to the same invoice. Deduplicating deliveries does not, by itself, establish the right invoice state.
What the local check demonstrated
The accompanying experiment and evidence record used Ruby 3.3.3, Active Record 8.1.3.1, Rack 3.2.7 and SQLite through the sqlite3 2.9.5 gem. It is a component fixture, not a deployed Rails endpoint. Its signing scheme is deliberately named a lab protocol; it does not implement Stripe’s signature format or exercise Stripe delivery.
The unsafe receiver returned 200 after adding an event to an in-memory list. Clearing that list left no receipt and no local projection. The corrected receiver persisted a pending receipt before returning 202. An injected queue error then left recoverable work in the database. A separate recovery pass found that receipt and completed the local update.
| Injected condition | Observed result in the fixture |
|---|---|
| Queue hint lost or unavailable | Pending database receipt remained discoverable |
| Repeated event and repeated processing | One receipt and one resource projection remained |
| Interruption before the local transaction committed | Projection rolled back; receipt remained pending |
| Old event delivered after a newer one | The fake authoritative resource supplied the current state |
| Resource unavailable | Processing failed visibly with the receipt still pending |
| Invalid signature or stale signed request | Request was rejected before storage |
| Same event ID with a different payload | Receiver returned a conflict instead of silently discarding it |
The ten checks passed with 34 assertions on 20 September 2026. The interruption is an injected exception, not a killed operating-system process. Reopening a database connection demonstrated persisted recovery state; it did not test storage corruption or host failure.
Choose the recovery contract explicitly
The fixture treats queue entries as hints. Its database receipt is the durable work list. That choice requires an independently operated sweep of pending records and a policy for poison events, retention, access, alerting and retries. None of those operational responsibilities disappears because the endpoint returns quickly.
Active Job’s transaction guidance explains why backend and database arrangement matter. A queue in another database cannot simply inherit the application’s transaction. Confirm the actual adapter and deployment before copying an enqueue pattern.
Here, the resource projection and processed marker share one local transaction. An external payment, email or accounting write would cross another boundary and require its own idempotency and reconciliation design. This example therefore makes no universal exactly-once claim. Its late-event test is sequential against a fixed fake resource. Separate receipts do not lock that resource together, so the fixture does not prevent regression from concurrent stale reads. It also does not justify holding a database lock during a slow network request.
For an existing application, define one receiver-to-result path and agree what accepted, pending, completed and failed mean. That produces a useful Rails integration delivery scope: implement the receipt and recovery behavior, execute the failure cases, and hand over the operating responsibilities along with the code.
Prepared and executed with AI assistance using synthetic inputs. Independent AI reviews are recorded separately from the byline owner’s approval; no human engineering sign-off is claimed.
Download the experiment
Download the runnable Rails examples and evidence summary (ZIP, 36 KB). This article’s example is in q04/; the shared setup and reproduction instructions are in README.md. The package contains eight synthetic examples, checked on 20 September 2026. The recorded runtime, provider fakes and limits are documented inside.