DEV Community

ShipTested
ShipTested

Posted on • Originally published at shiptested.github.io

One Webhook, Five Deliveries, One Entitlement

A customer can complete checkout once while your webhook handler receives the
same event more than once. If every delivery creates access again, one purchase
can produce duplicate licenses, seats, credits, or subscription records.

The launch check is concrete:

Take one valid, signed order webhook. Deliver the exact same body five times.
The result should still be one processed-event record and one intended
entitlement.

A webhook signature is necessary. It is not idempotency.

What the signature proves—and what it does not

A valid signature tells your handler that the raw request body matches a
message signed with the configured webhook secret. It helps reject forged or
modified payloads before they change state.

It does not tell you that the message is new.

Every repeated delivery can carry a valid signature because it is an authentic
copy of the same event. A handler that verifies the signature and then blindly
calls grantAccess() can still grant access five times.

In the deliberately vulnerable ShipTested example, the handler parses the body
and appends an entitlement on every call. Replaying the fixture five times
leaves five entries:

same order webhook ×5
entitlements.length === 5
Enter fullscreen mode Exit fullscreen mode

That is the failure we preserve as a reproducible red test.

Define the invariant before the implementation

The fixed example starts with an invariant:

For one accepted order_created event and one expected product variant,
repeated sequential delivery changes entitlement state once.

The handler verifies the HMAC signature against the unmodified raw body before
mutation. It then checks the fields this teaching fixture expects:

  • event name is order_created;
  • resource type is orders;
  • order ID is present;
  • payment status is paid;
  • store ID matches;
  • variant ID matches;
  • order and item test mode match the configured environment;
  • the custom internal user ID is a non-empty string.

A signed payload for an unexpected variant grants nothing. So does an invalid
signature.

After those checks, the example creates a stable key from the event name,
resource type, and order ID:

const eventKey = `${eventName}:${order.type}:${order.id}`;
Enter fullscreen mode Exit fullscreen mode

The first accepted delivery records that key and sets the intended entitlement.
Later deliveries with the same key return duplicate-no-op.

The five-delivery test

The fixed test signs one raw JSON body once, then sends that same body and
signature through the handler five times sequentially.

Its expected outcomes are explicit:

1. granted-once
2. duplicate-no-op
3. duplicate-no-op
4. duplicate-no-op
5. duplicate-no-op

processedEvents.size === 1
entitlements.size === 1
Enter fullscreen mode Exit fullscreen mode

That is stronger evidence than clicking through checkout once and looking at a
success page. The test forces the duplicate-delivery path and records the
invariant in code.

Clone the teaching harness and run:

npm test
npm run test:launch
Enter fullscreen mode Exit fullscreen mode

The repository contains both the vulnerable reproduction and the fixed
sequential replay test. The fixtures use test-only values and contain no live
credentials.

What this result actually proves

The test proves that this dependency-free JavaScript model handles five
sequential copies of the same signed fixture as one intended entitlement
change.

It also demonstrates that four controls solve different problems:

  • signature verification rejects a body with an invalid signature;
  • payload allowlisting rejects an authentic but unexpected order;
  • the event key recognizes a repeated accepted event;
  • the entitlement key represents the intended access state.

That is the evidence boundary.

What it does not prove

The public example stores processed events in an in-memory Set and
entitlements in an in-memory Map. A process restart clears both.

Its five deliveries run in a normal loop. They do not arrive concurrently. The
example therefore does not prove that two workers cannot pass a database
existence check at the same time.

It also does not prove:

  • that the custom user ID exists in your database;
  • atomic rollback after a failure between event recording and entitlement mutation;
  • behavior across multiple server instances;
  • refund or revocation ordering;
  • protection against a late purchase event restoring revoked access.

Those are launch checks for the real database-backed application, not claims
made by this harness.

In production, the “already processed?” check and the entitlement change need
a database-enforced boundary. Use a transaction and appropriate unique
constraints so two overlapping deliveries cannot both win. Resolve the buyer
against a real internal record. Then test forced failures, concurrent delivery,
process restarts, and refund ordering against the deployed database revision.

Do not label the in-memory demonstration production-safe. Do not treat one
green sequential replay as a security certification.

Run the smallest useful test

Start with the test that can fail today:

  1. capture one valid test-mode webhook without exposing its secret;
  2. replay the exact body and signature five times in a safe environment;
  3. inspect processed-event and entitlement state;
  4. save the result with the commit and environment tested.

If five deliveries create five grants, keep that reproduction. It is better
evidence than a vague feeling that retries are “probably handled.”

Inspect the exact test

Run the broader launch-readiness checklist

Sources: Lemon Squeezy webhook requests
and signing requests.


ShipTested uses AI heavily to produce this material and verifies the technical
examples by running them. There are no fake testimonials and no guaranteed
outcomes — the point is that you run the tests yourself instead of trusting an
author.

Top comments (0)