01  SIGNATURE; 02  ORDER MATCH; 03  CURRENT STATE. Editorial diagram for payment webhook acceptance.
Original technical illustration. Source-based local-demo discussion, not a product screenshot.

A valid payment webhook signature is one condition for accepting a notification. Before granting access, the merchant still needs to bind the event to its own order and check whether that order is eligible for delivery now. A correctly signed payment event can arrive after a full refund.

Reading StableIn's SDK alongside its merchant example makes that division unusually clear. The SDK checks the signed bytes. The merchant then checks the event structure, payment evidence and purchase details. Only after reading the current order does it decide whether to queue delivery. The implementation discussed here is a local demo using test assets, not a production security assessment.

Start with the bytes that arrived

StableIn signs the timestamp, a dot and the raw request body with HMAC-SHA256. Its verifier rejects a timestamp more than 300 seconds away from the supplied current time, compares the signature with a timing-safe comparison and checks that the event ID in the body matches the event ID header.

The raw body matters. Parsing JSON and serializing it again can change whitespace or key order. That can change the signed bytes while leaving the apparent meaning intact. Preserve the original bytes for verification before handing a parsed object to the rest of the application. Stripe documents the same raw-body requirement for its own webhook protocol, although its headers and event formats differ. Stripe webhook documentation.

The timestamp window also has a narrow job. It limits how old a signed delivery can be when received. It does not make an event unique. A repeated delivery inside the accepted window still needs persistent duplicate handling. The existing idempotency walkthrough covers that part.

A signed body can still fail the payment checks

In the merchant example, verifyNotice first calls the SDK verifier. It then validates the result against the webhook schema. For a payment.paid event, it requires a paid order and finalized payment evidence. It compares the payment's identity, chain, gateway, recipient, token, amount, fee and net amount with the corresponding order fields.

These checks answer different questions. A matching signature establishes that the bytes were signed with the shared secret. Schema validation establishes that the receiver understands the payload. Comparing the payment evidence with the order catches an internally inconsistent event.

The next comparison uses information that came from the merchant's own saved purchase. StableIn's example checks the local order reference, platform order ID when already bound, chain, gateway, token and raw integer amount. It also requires the expected confirmation policy. An amount that agrees with another field in the notification can still disagree with the price saved by the store.

Check Example of a rejection Merchant consequence
Raw-body signature Body changed after signing Stop before processing the payload
Timestamp and event header Delivery outside the accepted window or mismatched event ID Reject the delivery
Event schema Amount has an unexpected data type Do not interpret the event as a valid order update
Payment evidence Payment amount differs from the event's order amount Reject the inconsistent payment claim
Local order binding Event refers to a different token or purchase Keep the local purchase unchanged
Current eligibility Current order is fully refunded Do not create a new entitlement

Payment webhook checks from original request bytes through event validation, local order matching and current fulfillment eligibility

The checks follow StableIn's merchant example. The diagram separates notification acceptance from granting an entitlement.

Read the order before scheduling its delivery

A historical event describes a change that happened. The order API can provide a later view of the purchase. StableIn's receiver fetches that view, compares it with the local purchase and rejects an API snapshot older than the event it is processing. Its local projection also refuses to move backwards to an older revision.

The fulfillment predicate is small enough to inspect directly. This is the rule from the merchant example, reformatted for reading. It assumes the preceding schema and order checks have succeeded.

function canFulfill(order, localRevision) {
  return order.paymentStatus === 'paid'
    && order.refundStatus !== 'full'
    && BigInt(order.refundedAmount) < BigInt(order.amount)
    && BigInt(order.revision) >= BigInt(localRevision);
}

The receiver records the event and eligible pending work in a database transaction. A separate worker rechecks the order before granting the local entitlement. This gives a delayed notification another opportunity to encounter the newer refund state.

An API outage adds a tradeoff. Fetching current state adds latency and a dependency to the receiver. In this example, failure to obtain the required state prevents that transaction from completing. The sender needs a working retry path. A different architecture could durably accept a verified event first and reconcile it asynchronously, but it would need its own recovery rules. That alternative is not what this merchant example currently implements.

Put the difficult cases into the acceptance test

A useful demo should include a correctly signed event that the merchant refuses to use. Ask for a mismatched local amount, valid JSON with an invalid schema, and a paid event whose current order is fully refunded. Keep duplicate delivery as a separate case so the team can show which constraint prevents a second entitlement.

Download the acceptance worksheet and small signature exercise. The exercise uses synthetic data and Python's standard library. It demonstrates exact-byte signing and selected order checks. It does not implement StableIn's complete schema, HTTP receiver, durable deduplication or payment finality. Its test report accompanies the download. The four existing merchant rule tests were also rerun on 28 October 2026; the full chain and database suite was not rerun for this article.

For an integration review, StableIn Payments provides the checkout and merchant workflow behind these examples. Bring your current order format and the action that grants access. Those determine where the checks belong and which failure cases the delivery needs to pass.