24
61 Comments

I built a CLI to catch Stripe webhook regressions using real event fixtures — looking for 2–3 developers to try it

I’ve been building a small open-source CLI called Spoke Hooks.

The problem I’m trying to solve is pretty simple:

When a Stripe webhook breaks after a code change, the interesting failures often come from real event payloads — retries, duplicate deliveries, odd payload shapes, or business-specific data — not from the clean test payload you wrote six months ago.

Spoke Hooks lets you keep those events as regression fixtures.

The workflow is:

npm install -D [@spoke](/spoke)-labs/hooks

npx spoke-hooks init
npx spoke-hooks add stripe-event.json
npx spoke-hooks baseline
npx spoke-hooks test

baseline records how your current webhook handler responds.

After changing your code, test replays the same event. If the HTTP status or response body changes, it fails with a non-zero exit code, so it can run in CI.

Right now the scope is intentionally small: Stripe + Node.js + Express, local-only, with no account, hosted backend, dashboard, or cloud storage.

GitHub:
https://github.com/Spoke94/spoke-hooks

npm:

npm install -D [@spoke](/spoke)-labs/hooks

What I’m looking for right now is not feature ideas from people who haven’t used it yet.

I’d like to find 2–3 developers who already maintain a Stripe webhook handler and are willing to try it on a real or realistic project.

If you do, I’d especially like to know:

where the workflow feels awkward,
whether baseline + replay is actually useful,
and what stops you from keeping real webhook events as regression fixtures today.

I’m trying to validate the workflow before building anything bigger.

on September 17, 2026
  1. 1

    Great question. For payout reconciliation, HTTP status is only the surface.
    The real risk is silent accounting drift: your webhook returns 200 OK, but the ledger records the wrong net payout after fees, refunds and adjustments.

    What I’d want to assert is the final reconciled bank-facing number.
    Not just raw Stripe event fields, but the end value that matches what actually lands in the bank.

    This is exactly the pain MarginLens solves: turning Stripe CSV exports into that bank reconciliation bridge, without asking users to upload raw financial data to a server.

    1. 1

      That’s helpful — “the final bank-facing reconciled number” is a much clearer regression boundary than the raw event itself.

      It also reinforces the direction I’m leaning toward: Spoke shouldn’t need to understand every accounting model internally. A project-defined assertion hook could return that reconciled value, and baseline/test could compare it just like any other side-effect snapshot.

      Appreciate the concrete example.

  2. 1

    Keeping the scope to Stripe, Node, Express, and local fixtures makes the test clear. The part I'd hesitate on is capturing a real event safely: it may contain customer data or secrets I don't want committed beside the test. Does add show exactly what will be stored and offer a redaction step before the fixture is saved? That could decide whether a developer tries it on a real project instead of another clean sample.

    1. 1

      Yes — that’s exactly the trust problem I’m trying to solve next.

      The current release does not yet have a pre-save redaction flow, and I don’t want to pretend that raw production events are safe to commit as-is.

      The direction I’m leaning toward is for add to make the saved fixture explicit before persistence: show what will be stored, apply/default redaction rules, and let the developer review the result before it lands in the repo.

      I also want to keep a fast path for already-sanitized or synthetic fixtures so the common case doesn’t become an interactive wizard.

      Your point about this deciding whether someone uses a real event versus falling back to another clean sample is especially useful — that’s exactly the adoption behavior I want to avoid.

  3. 1

    The fixture-from-real-events idea is the right core. One thing I'd flag from running Stripe webhooks in production (disclosure: I build Mythex, an AI app builder, so this is from our billing path): comparing status code and response body catches handler regressions, but it misses the failures that actually hurt, which are side-effect regressions.

    Concrete case: a refactor that makes your handler return 200 correctly but stop writing the subscription row, or write it twice. Status and body are identical before and after. The money is wrong.

    Two things that would make me adopt this:

    • Let a fixture assert on state, not just the response. Even a simple "run this SQL or call this function after replay, compare against baseline" hook would cover it.

    • Replay the same event twice in one test, by default. Duplicate delivery is Stripe's most common surprise, and idempotency bugs only appear on the second delivery. If baseline recorded "first delivery: 200 + row created; second: 200 + no new row", that's the regression test people actually need.

    Also worth handling explicitly: signature verification. Most replay tooling bypasses it, which means the test passes and production rejects the event.

    Is Fastify in scope, or is Express-only deliberate for v1?

    1. 1

      This is exactly the distinction I’m converging on from the recent feedback.

      The response is only one part of the oracle — the expensive regressions are usually in state.

      The direction I’m leaning toward is:

      keep HTTP response checks as the zero-config baseline
      add an optional project-defined assertion hook that can run SQL, call a function, or return some serializable state
      store that state in the baseline and compare it after replay
      make duplicate replay a first-class scenario so the second delivery can explicitly assert “no additional side effect”

      I also agree on signature verification — I want the replay path to preserve it rather than bypass it, likely by re-signing the stored payload with a local/test secret at replay time.

      On Fastify: current V0 is deliberately scoped around Stripe + Node + Express, so I don’t want to claim Fastify support before I’ve tested it properly. The replay layer is HTTP-oriented though, so I’d like to keep the design framework-agnostic where possible rather than baking Express assumptions into the core.

      Really useful feedback — especially the “would adopt if…” criteria.

  4. 1

    This is the right way to validate a developer tool — find 2-3 people with the exact pain, not 200 who vaguely care. Stripe webhook regressions from real event shapes (retries, odd payloads, duplicate deliveries) are exactly the failures that bite you in prod. The clean fixture you wrote months ago never catches them. Interested in giving Spoke Hooks a run on a real handler if you still have slots open.

    1. 1

      Absolutely — still have room, and this is exactly the kind of test I’m looking for.

      Current V0 is Stripe + Node and the flow is:

      npm install -D @spoke-labs/hooks
      npx spoke-hooks init
      npx spoke-hooks add <event.json>
      npx spoke-hooks baseline
      npx spoke-hooks test

      One important warning before you use a production-derived event: automatic PII redaction isn’t in the current release yet, so please sanitize any real customer data before committing the fixture.

      What would help me most is feedback from the real handler itself — where init → add → baseline → test feels awkward, whether the replay catches anything your existing tests miss, and whether you’d actually leave it running in CI afterward.

      Thanks for offering to give it a real run.

  5. 1

    This sounds like one of those tools that solves a problem developers don’t think about until something breaks in production. Stripe integrations can look perfectly fine during normal testing, but webhook behavior and edge cases are exactly where small changes can create very expensive bugs.

    Using real event fixtures sounds especially useful because it makes the testing environment closer to what actually happens in production instead of relying only on manually constructed payloads. I’d also be interested in how you handle changes in Stripe API versions and whether the tool can compare expected behavior between releases. Definitely feels like the kind of developer tool where a few strong early users could give you much better feedback than trying to attract hundreds immediately.

    1. 1

      Thanks — that’s exactly the kind of problem I’m trying to keep the first version focused on.

      API-version drift has come up repeatedly now, so I’m leaning toward storing version/freshness metadata with each fixture rather than treating the payload as timeless.

      I also like the “compare expected behavior between releases” framing. If a baseline changes intentionally, the tool should make that change visible rather than silently replacing the old expectation.

      And I agree on the early-user point — I’d rather learn deeply from a few real integrations than optimize for broad reach before the workflow is proven.

  6. 1

    Great scope decision - Stripe+Node+local-only is the right v1. One regression class I keep hitting building payment webhook handlers (I run x402 payment endpoints, same failure profile): the handler passes in dev because in-memory state carries between events, then fails on a fresh deploy where idempotency has to come from persisted event IDs. The highest-value baseline fixtures are a duplicate-delivery pair plus a retry-after-5xx sequence, replayed in a fresh process each run - that catches the statefulness bug a single-event replay can't. Also treat the replayed request like prod: re-verify the captured signature before comparing response bodies, or a signature-check refactor shows up as a fake webhook regression when it's really the fixture losing its header.

    1. 1

      This is a really useful distinction.

      The “fresh process” point is especially important — a handler can look idempotent in development simply because in-memory state survives between events, while a fresh deploy exposes that nothing was actually persisted.

      I’m starting to think duplicate delivery and retry-after-5xx should be modeled as sequences rather than isolated fixture replays, with the option to reset the app/process between runs.

      And agreed on signature verification: I want the replay path to stay as close to production as possible rather than teaching users to bypass verification.

      Thanks — this adds a useful process-lifecycle angle to the idempotency problem.

  7. 1

    Great scope decision - Stripe+Node+local-only is the right v1. One regression class I keep hitting building payment webhook handlers (I run x402 payment endpoints, same failure profile): the handler passes in dev because in-memory state carries between events, then fails on a fresh deploy where idempotency has to come from persisted event IDs. The highest-value baseline fixtures are a duplicate-delivery pair plus a retry-after-5xx sequence, replayed in a fresh process each run - that catches the statefulness bug a single-event replay can't. Also treat the replayed request like prod: re-verify the captured signature before comparing response bodies, or a signature-check refactor shows up as a fake webhook regression when it's really the fixture losing its header.

  8. 1

    Free sample if useful: https://corymaynard.gumroad.com/l/clhrdw — I made it after getting burned by the same kind of “worked in test, died in live” webhook issues. This CLI/fixtures approach is a much better way to catch them earlier.

    1. 1

      Thanks — appreciate the context and the kind words. I’m keeping the current validation focused on direct developer usage and feedback for now.

  9. 1

    Nice focus on replaying real payloads—one extra failure mode I’ve found useful is treating webhook handlers as untrusted input: verify the signature before parsing business fields, persist event IDs for idempotency, and test out-of-order delivery. I keep a small pass/fail checklist for auth, object-level access, secrets, APIs, uploads, and webhooks here if useful: https://corymaynard.gumroad.com/l/clgtpy

    1. 1

      Thanks — agreed on treating webhook input as untrusted and keeping signature verification in the real replay path.

      Idempotency and out-of-order delivery have both come up repeatedly in the feedback as well, so they’re definitely part of what I’m validating now.

      Appreciate the checklist reference.

  10. 1

    Using real Stripe event fixtures to catch webhook regressions sounds like a practical approach, especially since small changes can easily break payment-related integrations. It would be interesting to see how the CLI handles different event types and whether it can fit into existing CI/CD workflows without much setup.

    1. 1

      Thanks — those are exactly two of the things I’m trying to keep simple.

      The current V0 doesn’t hard-code a specific Stripe event type; it stores and replays the event JSON fixture against your local webhook handler, so the same workflow can be used for different event types.

      For CI, the goal is intentionally boring: install the npm package, keep the fixtures in the repo, run spoke-hooks test, and let the exit code gate the pipeline. No hosted backend or dashboard is required.

      I’ve already got the basic flow running in GitHub Actions, but the part I’m validating now is whether that setup stays frictionless on real codebases rather than just in the demo project.

  11. 1

    One practical blocker for keeping real events as fixtures: the captured Stripe-Signature header has a TTL (roughly 5 minutes of tolerance in constructEvent), so replaying a raw event against a handler that verifies signatures will fail for the wrong reason. Either re-sign the payload with a local test secret on replay, or inject below the verification layer and assert on the handler body directly — otherwise the first replay teaches people to disable signature checking in test, which is worse than no fixtures. Same lesson as the fixture-drift point above: store the Stripe API version next to the event type, since payload shape meaning drifts with it.

    1. 1

      This is a strong point, and you’re the second person to call out signature verification specifically, so I’m treating it as a real compatibility requirement now rather than just a docs issue.

      I agree that replaying the originally captured Stripe-Signature is the wrong behavior — it can fail for timestamp freshness instead of application behavior and teaches people to disable verification in tests.

      My preferred direction is to keep the exact stored payload, then re-sign it at replay time with a configured local/test webhook secret so the real verification path still runs.

      I’d rather preserve end-to-end behavior than bypass signature checking by default.

      And yes, stripe_api_version should live next to the fixture metadata as part of the same replay contract.

  12. 1

    Completely agree with this perspective. Keeping things simple early on really helps avoid over-engineering. Thanks for sharing!

    1. 1

      Thanks — that’s exactly what I’m trying to protect right now.

      The temptation is to keep adding features once people start suggesting good ideas, but I’d rather prove the smallest useful workflow with real users first.

  13. 1

    One subtle failure mode is fixture drift: a real payload captured against one Stripe API version can change meaning later. I’d store the event/API version and fixture schema version in baseline metadata so stale fixtures are flagged—are you planning that kind of freshness check?

    1. 1

      Yes — this kind of lightweight freshness check is starting to look increasingly justified.

      I don’t want to build a full fixture-versioning subsystem, but storing metadata like Stripe API version, capture/import time, and a fixture schema version feels like a useful minimum.

      That would at least let Spoke distinguish “this fixture is old/stale relative to the current contract” from “this regression is real.”

      The part I haven’t decided yet is how strict the tool should be.

      Would you expect an API-version mismatch to emit a warning and let the test continue, or should CI fail hard until the fixture is explicitly reviewed/re-baselined?

      1. 1

        I’d warn and continue in local/dev, and fail in CI on main. A stale fixture that silently “passes” is worse than a noisy fail. Store stripe_api_version, captured_at, and schema_version on every fixture; on mismatch, fail with “refresh fixture” in the message.

        1. 1

          That makes sense, and I like the asymmetry:

          local/dev: warn and continue
          CI: fail loudly

          The metadata fields you listed also feel like the right minimum:
          stripe_api_version, captured_at, and schema_version.

          I’d probably avoid hard-coding the branch name itself and make the strictness depend on CI context or config, so teams using non-main default branches don’t get surprising behavior.

          But the principle is clear: a stale fixture should never silently look healthy in CI.

          And “refresh fixture” as the failure action is exactly the kind of message I’d want the tool to give.

  14. 1

    One adoption detail I’d measure is the time from a production incident to a safe fixture: if redaction or naming takes longer than writing a regression test, the workflow may be abandoned. A small manifest recording event type, API version, expected side effects, and why the fixture exists could keep the collection understandable after a few months. I’d also treat duplicate delivery and async completion as separate assertions, since an HTTP 200 alone won’t catch those failures.

    1. 1

      That’s a useful way to frame it — “time from incident to safe fixture” may actually be one of the better adoption metrics for this workflow.

      If capturing, sanitizing, naming, and understanding the fixture takes longer than writing a normal regression test, I can see teams abandoning it quickly.

      I also like the lightweight manifest idea more than jumping straight to a full fixture-versioning system. Event type, API version, expected side effects, and a short “why this fixture exists” note would probably make the corpus much easier to maintain over time.

      And agreed on duplicate delivery vs async completion — I’m starting to think those need separate assertion semantics rather than being collapsed into one generic HTTP check.

      Would you expect that metadata to live inside each fixture JSON itself, or in a separate manifest file for the whole fixture set?

  15. 1

    The pain is real. I migrated my payment webhook last week (Paddle,
    not Stripe) and the scary part wasn't broken payloads — it was
    silently wrong ones: 200 OK, half-processed state, nobody notices
    for days.

    Event fixtures would've caught what my unit tests didn't. Happy to
    be one of the 2–3 — my stack is Next.js on Vercel, webhook handles
    subscription state and credit top-ups, so it sees the full event
    zoo including retries.

    1. 1

      Thanks — that 200-OK-but-partially-applied-state failure is exactly the kind of case the recent feedback has been pointing toward.

      I really appreciate the offer to be one of the 2–3 testers.

      One important constraint though: the current V0 is intentionally Stripe-only, so I don’t want to waste your time by pretending Paddle is supported yet.

      Your example is still very useful because it reinforces that the real regression oracle has to cover side effects, not just the HTTP response.

      If you also maintain any Stripe webhook handlers, I’d be very interested in having you try the current version. Otherwise, I’d be happy to keep you in the loop for a later Paddle/Next.js pass once the Stripe workflow is proven.

      And the Next.js/Vercel setup is useful context too — I’m seeing that the same problem exists beyond long-running Express handlers.

      1. 1

        Fair call — Stripe-only it is. All my handlers are
        Paddle, so I'll gladly take the "keep me in the loop"
        option.

        Two fixtures worth writing when you get to the Paddle
        pass:

        1. subscription.updated where items[0].price.id
          changes — that's a plan switch, and the side effect
          (feature flags, credit top-ups) must re-evaluate, not
          just refresh status. Same event type, different
          behavior depending on which field moved.

        2. The identical webhook delivered twice (their
          retries). Your oracle should treat delivery #2 as a
          no-op for anything already applied. That's the
          difference between "tests pass" and "production works"
          on serverless.

        Good luck with the Stripe pass — rooting for it.

        1. 1

          This is extremely useful — thank you.

          The subscription.updated example is a great reminder that event type alone is not enough; the business meaning can change based on which field moved inside the payload.

          And the duplicate-delivery case lines up with the strongest feedback I’ve been getting so far: the second delivery shouldn’t just return 200, it should leave already-applied side effects unchanged.

          I’ll keep both of these as concrete Paddle cases for later, and I’ll keep you in the loop once the Stripe workflow is proven and I start looking at a Paddle/Next.js pass.

          Really appreciate the specific examples.

  16. 1

    The real event fixtures angle is the right call.

    Mocked payloads for webhook testing is exactly how most regressions get missed — the edge case lives in a real payload shape from a real customer flow, not the happy path fixture you wrote 8 months ago. The gap between "tests pass" and "production works" in webhook-heavy systems is almost always a payload schema problem that only shows up on a Monday morning.

    Have you thought about a shared fixtures library as a longer-term direction? Teams with similar Stripe integrations often have near-identical webhook shapes and would genuinely pay to not rebuild this themselves. The person who curates the canonical real-world fixture set for common Stripe events has something competitors can't easily replicate.

    What's the most surprising edge case you caught with real fixtures that the mocked version would have missed?

    1. 1

      Thanks — I agree the shared fixture library could be an interesting longer-term direction, especially if the events are canonicalized and scrubbed rather than treated as raw production payloads.

      I’m deliberately not expanding into that yet though. Right now I’m trying to prove the local workflow with real users first and solve the more immediate problems around sanitization, side-effect assertions, and fixture lifecycle.

      On the edge-case question, I don’t want to invent a production story I haven’t earned yet.

      So far, Spoke Hooks has caught intentional regressions in the local proof, while several developers here have described real failures around duplicate delivery, idempotency, out-of-order events, and payload/version drift.

      Getting the first real user to come back and say “this fixture caught something my normal tests missed” is actually one of the signals I’m trying to reach next.

  17. 1

    The scope discipline here is right, but I think the weak point is the oracle rather than the fixtures. Most Stripe handlers return 200 with an empty body no matter what happens internally, so status plus response body will stay green through exactly the regressions people care about: the subscription row that stopped being updated, the receipt email that now fires twice, the idempotency key that quietly changed shape. If you let the project register an optional assertion hook that returns a serializable snapshot of side effects, or let people plug in a recorder for outbound calls, then the diff starts covering the failures that actually cost money, and it does that without you needing a dashboard or a hosted backend. Two smaller things I would want documented before trying it on a real handler: how you deal with signature verification on replay, since a stored real event has a stale stripe-signature and timestamp and most handlers reject it outright, whether you re-sign with the local test secret or expect people to bypass the check in test mode; and whether replaying the same fixture twice in a row is a first-class mode, because duplicate delivery is the most common production bug in this area and you would get that assertion almost for free. Which direction are you leaning on the assertion surface, staying strictly HTTP-level to keep the tool trivial to adopt, or going after side effects and accepting the config cost?

    1. 1

      This is exactly the direction I’m leaning now.

      I want the HTTP-only path to stay zero-config, but the repeated feedback is making it clear that the useful regression oracle has to be able to observe side effects.

      The smallest version I’m considering is an optional project-defined assertion hook that returns a serializable snapshot of application state. baseline would store that snapshot, and test would compare it after replay. That keeps Spoke Hooks database-agnostic while still covering things like row counts, idempotency records, balances, or outbound-call summaries.

      The signature-verification point is important too. I don’t want to pretend stale production signatures will work on replay. I’m going to inspect and define that behavior explicitly before claiming real-handler compatibility.

      My current instinct is that replay should be able to re-sign the stored payload locally using a configured test webhook secret, rather than asking users to disable verification.

      And yes, replaying the same fixture twice should probably become a first-class idempotency scenario rather than something users have to script manually.

      Would a generic snapshot/assertion hook plus local re-signing be enough for you to try this on a real handler, or would you expect outbound-call recording to be part of the first usable version too?

  18. 1

    Testing webhooks properly is always a pain point during integration, especially with edge cases. Clean approach with event fixtures! Wish you luck with the beta tests.

    1. 1

      Thanks — appreciate it!

      Webhook edge cases are exactly what pushed me toward using real event fixtures instead of relying only on clean mocks.

      I’m keeping the beta intentionally small for now and trying to learn from real usage before expanding it.

  19. 1

    The real-event fixture approach is a practical way to catch webhook regressions that clean mocks miss, especially around retries and duplicate deliveries. I have seen similar issues when automation and coding agents replay stale payloads—how are you thinking about fixture versioning when the handler contract changes?

    1. 1

      That’s a good question.

      Right now, V0 intentionally keeps fixtures as plain JSON in the repo, so there’s no built-in fixture versioning beyond Git history.

      I’m starting to see a broader lifecycle problem emerge though: fixtures can become stale when the handler contract or upstream payload shape changes, and blindly re-baselining them can hide the very regressions the tool is supposed to catch.

      I don’t want to jump straight into a versioning subsystem yet, so I’m curious what the minimum useful behavior would be for you.

      Would recording metadata like the Stripe API version / capture time and warning when a fixture looks stale be enough, or would you expect explicit fixture versions tied to handler contract changes?

  20. 1

    The ask for 2-3 real users before building anything else is the part most tool builders skip. Framing it as "not looking for feature ideas from people who haven't used it" is unusually disciplined.

    We build an SEO auditing tool and the failure mode is similar: a code change that passes every test but silently produces different output on real data. Synthetic test cases cover the obvious paths. The edge cases that actually break production come from data shapes you didn't know existed until a user hit them.

    The question I'd push on: once someone has twenty fixture files, do they ever update the baseline? The danger with snapshot-style testing is that the baseline becomes a sacred artifact nobody touches, and "just update the baseline" becomes the new "just skip the test." PII was mentioned in another comment — I'd add that fixture rot is the other adoption barrier.

    1. 1

      That’s a really good point.

      I’ve been thinking mostly about fixture capture and replay friction, but fixture rot / baseline drift is a different failure mode entirely.

      If teams can respond to every failure with “just update the baseline,” the regression signal eventually loses its value.

      I don’t want to jump straight into building baseline-management features from one comment, so I’m curious: what would make baseline updates feel trustworthy to you in practice?

      Would you expect explicit per-fixture diffs, a separate approval step, or something else that forces the developer to review exactly what changed before accepting a new baseline?

  21. 1

    This is a great approach. The biggest pain with Stripe webhooks is definitely the weird edge cases in live payloads that the official mock events never cover.

    To answer your question on what stops me from keeping real events as fixtures today: sanitizing PII.
    Pulling a real event means I have to manually scrub customer emails, names, or sensitive metadata before committing the JSON to my repo. It's usually too much friction.

    If your CLI eventually adds a way to auto-sanitize or redact sensitive fields when saving the fixture, that would be a massive game-changer.

    I maintain a few Node.js SaaS apps. I'll try to give it a spin locally this weekend and let you know how the baseline workflow feels on a real codebase. Keep it up!

    1. 1

      Thanks — this is exactly the kind of feedback I was hoping to get.

      And yes, the PII/sanitization point has now come up repeatedly. The current release does not auto-sanitize production fixtures yet, so please don’t commit unsanitized real customer data.

      If you do try it this weekend, even using a manually sanitized or realistic fixture is enough for me to learn from the workflow.

      I’d be especially interested in where init → add → baseline → test feels awkward on a real codebase, and whether the baseline/replay model feels useful enough that you’d actually leave it in CI.

      Thanks for giving it a real try — I’d genuinely value the feedback.

  22. 1

    Hi, I do QA on small SaaS apps, and webhook handlers are where I find the most expensive bugs, so this caught my eye. Two cases I'd add next to duplicates: events arriving out of order (invoice.paid before the subscription.created your handler expects), and the same event under a newer API version, since the payload shape follows the endpoint's pinned version. One practical worry: real fixtures carry customer emails and addresses, so a scrub step before they land in the repo would help teams say yes. What I can't judge from the README is how it behaves with async handlers that ack first and process later. Thanks for keeping the scope this tight.

    1. 1

      Thanks — this is very useful.

      The duplicate/idempotency case has now come up from several people, and your point about async handlers adds an important constraint to the side-effect assertion design.

      You’re right that an immediate assertion after the HTTP response is not enough if the handler acknowledges first and processes later. I’m looking at the assertion layer as something that may need to wait/poll for application state rather than assume the side effect is synchronous.

      The scrub step is also moving from “nice to have” to a V0 requirement based on the feedback so far.

      I’m keeping out-of-order delivery and API-version drift in scope as validation scenarios for now rather than expanding the implementation too quickly.

      For async handlers, would a configurable assertion timeout/poll command be enough in practice, or would you expect Spoke Hooks to understand the queue/job system directly?

  23. 1

    The retry that still returns 200 is the case that got us once: a duplicate event applied twice while every clean test payload passed. One thing I'd want before wiring this into CI is a way to assert side effects (row counts in a test DB), not just status/body diffs. Is that on the roadmap?

    1. 1

      Yes — this is exactly the kind of case that makes status/body checks insufficient.

      A duplicate delivery that still returns 200 but applies the same side effect twice would currently slip through V0.

      I’m now looking at side-effect assertions as the next focused step, but I want to keep the first version generic rather than baking in a specific database.

      Would something like a post-replay assertion hook be enough for you?

      For example, after replaying the fixture, Spoke Hooks could run a user-provided command/script that checks your test DB state — row counts, balances, idempotency records, etc. — and fails CI if that command exits non-zero.

      If that would be enough for you to wire it into CI, that’s very useful to know.

  24. 1

    Response diffs will miss the bug this is built for: a retry that still returns 200 while applying the event twice. I'd want baseline to capture side effects, not just status and body, and fail the replay when the same event.id is processed again. Is the check intentionally response-only for now?

    1. 1

      Yes — the response-only check is intentional in the current V0. It was the smallest useful regression signal for proving the replay workflow, but the feedback here is making it clear that it is too weak for idempotency failures.

      I agree with the invariant you’re pointing at: receiving the same event.id again should be allowed, but replaying it should not produce an unexpected second side effect.

      I’m now looking at the next step as a generic side-effect assertion layer rather than a database-specific integration. For example, after replaying an event, Spoke Hooks could run a user-provided assertion command that checks DB/application state and fails CI if the state is wrong.

      Would that be enough for your use case, or would you specifically want Spoke Hooks itself to understand and baseline database state?

      1. 1

        A user-provided assertion command after replay would cover my use case — especially if it can compare against a baseline captured on the first delivery of that event.id (or an explicit seed state). I wouldn't need Spoke Hooks to understand the database schema itself; just a reliable hook to run the check against post-replay state and fail CI when it drifts. Built-in DB baselining would be nice later, but the generic assertion layer is the higher-leverage next step.

  25. 1

    The workflow is concrete, but the real test is whether developers keep using real-event fixtures after the first setup. Does baseline + replay become part of CI naturally?

    1. 1

      Exactly — I think that’s the more important test than whether the first setup works.

      I know the baseline + replay workflow works technically in CI, but I don’t have external evidence yet that teams keep maintaining the fixture corpus over time. That’s one of the things I’m trying to validate before expanding the product.

      My hypothesis is that it only sticks if adding a new production event is cheap enough that developers treat it like adding a regression test after a bug — not like maintaining another test system.

      Have you used any fixture/snapshot-based workflow that actually became part of the team’s CI habit? I’d be interested in what made people keep it up versus abandon it after the first setup.

      And if you maintain a Stripe webhook handler yourself, I’d be very interested in having you try the current workflow and tell me whether it feels like something you’d actually leave in CI.

      1. 1

        That “leave it in CI” test is exactly the signal I’d be interested in seeing. What’s the best email to reach you on?

        1. 1

          Absolutely — you can reach me at spoke9412@gmail.com.

          I’d really appreciate it if you try the current version in a real or realistic Stripe/Node project, especially with the idea of leaving spoke-hooks test in CI rather than just running it once.

          I’m most interested in where the workflow becomes annoying, what would stop you from keeping it in CI, and whether maintaining the real-event fixtures feels natural over time.

          Feel free to email me whenever you get a chance to try it.

          1. 1

            Thanks! I’ve just sent it over.

            Looking forward to hearing your thoughts whenever you have a chance.

  26. 1

    The thing that decides whether a team can adopt this is redaction at the add step. Real Stripe events carry emails, names, addresses and card metadata, so spoke-hooks add is effectively asking developers to commit customer data into git, which is an instant no from anyone who has to pass a security review. Ship a default scrub of known PII fields with an allowlist for the business fields people actually assert on, and this goes from a personal tool to something usable inside a company.

    1. 1

      That’s a fair point, and I agree this is a real adoption blocker for teams with security review requirements.

      Right now, spoke-hooks add imports the event JSON as-is — there is no built-in redaction step yet. So today the developer is responsible for sanitizing production events before committing them.

      A default scrub of known PII fields, with an explicit allowlist for fields the team wants to preserve, sounds much closer to the workflow I’d want for company use as well.

      I’m deliberately trying not to add features before I know they unblock real usage, so I’d like to ask one thing: if spoke-hooks add redacted known PII by default and let you keep selected business fields, would that be enough for you to actually try it in a real project?

      Also, are there any fields you would definitely want preserved by default for webhook regression testing?

  27. 1

    Same problem from the reconciliation side. When you replay real events, do you also keep fixtures for payout., refund., and balance_transaction.* — not just charge.*? Those are the events where a payload shape change can quietly break the books: fee breakdowns shift, disputes land after a payout settles, and suddenly the CSV does not tie out. Curious whether you've seen that in the wild.

    1. 1

      That’s a really good point.

      Spoke Hooks isn’t limited to charge.* events — the fixture/replay flow works with generic Stripe event payloads, so payout.* and refund.* shaped events can be preserved and replayed as well.

      What I haven’t validated yet is the reconciliation-specific failure mode you’re describing. V0 currently compares the webhook handler’s HTTP status and response body, so if the handler still returns the same response but silently produces the wrong ledger/reconciliation state, that would not be caught yet.

      I also haven’t personally seen that exact payout/reconciliation breakage in the wild yet, so this is exactly the kind of case I’m trying to learn from.

      If you have a sanitized payout/refund example, I’d be very interested to know what output you would actually want to assert on — ledger rows, fee breakdowns, exported reconciliation data, or something else. That would help me understand whether the useful regression boundary is the HTTP response or the downstream accounting effect.