Metrivo
Back to blog

payment webhook audit checklist

Payment Webhook Audit Checklist: Prove Your SaaS Revenue Pipeline Works

Audit a SaaS payment webhook from signed delivery to reconciled revenue. Test duplicates, retries, ordering, money states, recovery, and evidence gaps.

18 min read
Payment Webhook Audit Checklist: Prove Your SaaS Revenue Pipeline Works - Metrivo guide cover illustration

Payment webhooks sit at an awkward boundary: another company decides when to send the request, the network decides when it arrives, and your code decides whether the event changes access, invoices, subscription state, revenue reporting, or customer communication. A happy-path sandbox purchase can pass while the production pipeline remains vulnerable to a duplicate, a timeout after commit, a late refund, an older subscription update, or a live endpoint that was never registered.

The right audit therefore follows evidence, not files. Start at the provider dashboard, trace one event through authentication and durable receipt, prove the business effect, and then compare the local result with provider-side truth. This checklist is written for a founder or engineer who needs to answer a narrow question: if a real customer pays, fails, renews, refunds, or disputes a charge, can we explain what the provider sent, what our system accepted, what changed, and what still remains unknown?

The procedure is provider-aware but not tied to one framework. Stripe, Paddle, Razorpay, Dodo Payments, and Lemon Squeezy document different headers, event names, retry behavior, and testing tools. Treat those differences as configuration in a shared audit contract rather than forcing every provider into the same implementation details. The common contract is authenticity, durability, idempotency, monotonic state, correct money semantics, recoverability, and reconciliation.

What a passed payment webhook audit actually proves

Concise answer

A passed audit proves that one provider fact can travel from signed request to correct, recoverable business state without being forged, lost, counted twice, or misclassified.

Do not define success as receiving a test event. That proves routing and little else. A useful audit names the control at each boundary, the observable evidence it leaves behind, and the failure that the control prevents. If a check has no inspectable output, it is an assumption rather than a control.

The smallest complete evidence packet contains the provider event identifier, event type, provider object identifier, provider occurrence time, receipt time, verification result, processing status, related internal record, and final money or lifecycle state. It should be possible to search by either the provider identity or the internal identity. Secrets, full payment credentials, and unnecessary personal data do not belong in that packet.

Seven audit layers and their pass evidence
LayerPass evidenceFailure it exposes
1. DestinationCorrect live URL and intended event subscriptionsEvents sent nowhere, to staging, or never subscribed
2. AuthenticityValid request accepted; invalid or altered request rejected before side effectsForged events or body-parser signature failures
3. ReceiptAccepted event has a durable, searchable receipt before acknowledgementProvider sees success while the event is lost locally
4. IdempotencyReplay and concurrent delivery produce one business effectDuplicate payment, entitlement, email, or commission
5. OrderingOlder events cannot overwrite a newer terminal stateCanceled, paid, or refunded state rolls backward
6. Money semanticsOnly supported settled or renewal events enter revenueAuthorization or lifecycle activity inflates revenue
7. RecoveryFailed work is visible, replayable, and checked against provider truth where supportedSilent gaps survive after retries stop

1. Verify the destination, mode, and event contract

Concise answer

Confirm the exact production URL, mode-specific secret, subscribed event set, API version, and ownership scope before testing handler code.

Begin in the payment provider, not in your repository. Copy the destination URL and resolve it exactly as the provider will. Redirects, a staging hostname, an expired tunnel, a protected preview deployment, or a route that accepts the wrong HTTP method can all make correct handler code irrelevant. Check that test events go to test infrastructure and live events go to production. Providers commonly separate test and live credentials, destinations, products, or stores; crossing those boundaries can create a test that passes while live money never reaches the handler.

Next, list the events your application actually needs. The list should be derived from business decisions: grant access after a supported settled event, update renewal state after a supported renewal event, record failures for recovery, subtract refunds according to your accounting policy, and track subscription lifecycle without counting it as new money. Subscribing to every available event increases noise and makes unsupported events easier to process accidentally. Subscribing to too few events leaves obvious gaps such as renewals or refunds.

Record the provider API or webhook version attached to the destination. A payload fixture is only useful when it matches the version sent in production. For each subscribed event, write down the provider event name, the provider object it carries, the timestamp that represents when it happened, and the intended local effect. If the last column says ‘update everything,’ the contract is still too vague to audit.

Destination checks

Issue a provider-generated test delivery and retain the delivery record, HTTP response, and matching local receipt. Then test the deployed production route separately with the provider's safe live verification mechanism if available. A sandbox delivery does not prove the production secret, hostname, firewall, runtime, or subscription configuration.

  • The destination is HTTPS, public, and does not redirect.
  • The registered route and deployed route match character for character.
  • Test and live modes use separate secrets and the expected accounts or stores.
  • Only required events are subscribed, with an owner for every local effect.
  • The payload fixture matches the configured provider version.

2. Prove signature verification uses the untouched request

Concise answer

Verify the provider signature against the exact request bytes and the correct endpoint secret before parsing or applying any business effect.

A webhook endpoint is public by design. Knowing its URL must not be enough to create a paid order, extend access, issue a referral commission, or change subscription state. The audit must send a valid signed payload, a payload with the signature removed, a payload signed with the wrong secret, and a valid payload altered by one byte after signing. Only the authentic request should cross the trust boundary.

Raw-body handling is a recurring source of false failures. Stripe, Razorpay, Paddle, Lemon Squeezy, and Dodo all base verification on the received payload and provider-specific signature material. A framework that parses JSON and then serializes it again may change whitespace or key ordering. Put signature verification before any middleware or helper that transforms the body, and test the deployed runtime rather than assuming local behavior matches it.

Where the provider signs a timestamp, test an expired timestamp and clock skew. Timestamp checks reduce replay exposure, but they do not replace idempotency: a duplicate delivered inside the accepted window is still authentic. Secret rotation also needs a planned overlap or retry policy. Razorpay explicitly notes that older retried requests may need the old secret after a rotation; the exact rotation behavior must come from the provider you use, not a generic recipe.

  • Invalid, missing, expired, or mismatched signatures produce no durable business side effect.
  • Verification uses the raw bytes and the secret associated with this exact destination.
  • Comparison uses the provider SDK or a constant-time method where the provider documents one.
  • Logs record a safe verification outcome and event context without writing secrets or unnecessary payload data.
  • Rotation behavior is tested with in-flight retries rather than discovered during an incident.

3. Separate durable receipt from business processing

Concise answer

Acknowledge only after the authentic event is safe to recover; move slow or failure-prone business work out of the request path.

Provider documentation consistently rewards a short receive path. Paddle requires a 200 response within its documented window, Stripe advises returning a successful status before complex logic, Dodo recommends immediate acknowledgement with asynchronous processing, and Lemon Squeezy recommends storing events so processing can continue outside the request. The exact timeout differs, so copy the provider's current rule into your runbook instead of inventing a universal number.

Fast acknowledgement does not mean blind acknowledgement. The safe sequence is: read the raw body, verify authenticity, validate the minimum envelope, persist a durable receipt or enqueue transactionally, then return the provider's accepted success status. If the process returns success before the event is durable, a crash can create an event the provider considers delivered but your application cannot replay. If it performs email, entitlement, analytics, affiliate, and database work before replying, a slow dependency can trigger retries after some side effects already committed.

Force failures at each boundary. Stop the worker after receipt but before processing; the event should remain pending. Fail after the primary payment row is written but before a secondary action; retry should complete missing work without duplicating the row. Return a temporary server error before durability; the provider or simulator should retry. These tests reveal whether your status fields describe receipt, processing, and final application separately or collapse them into one misleading ‘success.’

4. Make exact replays and concurrent duplicates harmless

Concise answer

Use a stable provider identity backed by an atomic uniqueness rule, then prove both sequential and concurrent duplicates produce one final effect.

Duplicates are expected behavior, not a provider defect. Stripe says endpoints can receive an event more than once. Paddle guarantees at-least-once delivery and recommends the event ID as the deduplication key. Razorpay exposes a unique event ID header, and Dodo documents a webhook ID for idempotency. The precise key differs, but the audit question is the same: what stable identity survives retries, and where is uniqueness enforced?

An in-memory set or a read-then-insert check is not enough. Two workers can read ‘not processed’ at the same time and both apply the effect. Put uniqueness in the durable store or use a transaction that claims the event atomically. Decide what a duplicate returns: usually an already accepted event should receive a success response while the receipt records that no second effect ran. That avoids encouraging more retries for work already complete.

Test more than the payment table. A duplicate may leave the payment row unique while sending two receipts, granting two credits, firing two affiliate commissions, incrementing a metric twice, or enqueueing two downstream jobs. Enumerate every effect the event can cause and assert that each occurs once. Then deliver the same signed event simultaneously from two clients. A sequential replay test does not expose the race that matters.

Idempotency test matrix

Run the same event once, twice in sequence, twice concurrently, after a worker crash, and after a manual replay. Repeat with two distinct provider events that refer to the same payment object, because Stripe notes that separate event objects can sometimes describe the same underlying object. Your event-level and object-level rules solve different duplicate classes.

5. Stop late events from rolling state backward

Concise answer

Order business state by provider occurrence time, version, or valid state transitions—not by the moment the HTTP request arrived.

Related webhook events can arrive out of order. Stripe does not guarantee event order. Paddle tells consumers to use occurred_at rather than arrival time, and Razorpay warns that webhook order may differ from the ideal payment sequence. The failure is subtle because every individual handler can be correct while their combined arrival order produces the wrong final state.

Build reversed-sequence fixtures from real provider payloads. Deliver captured before authorized, canceled before an older updated event, refunded before a delayed paid notification, and renewal failure around a later recovery. The correct rule depends on the provider and domain. Some states are monotonic, some require the latest provider timestamp, and some should trigger a provider read because the event snapshot is insufficient. Do not impose one guessed state machine on every provider.

Also test equal timestamps, missing optional timestamps, delayed manual replay, and a newer event processed before an older event leaves the queue. The final local state should match the newest valid provider fact, while the audit trail preserves the late event and explains why it did not overwrite current state. Silently discarding it without a reason makes incident reconstruction harder.

6. Map provider activity to money without inflating revenue

Concise answer

Keep authorization, settlement, renewal, refund, dispute, failure, and subscription lifecycle as distinct canonical states; include money only under an explicit revenue rule.

A reliable transport can still produce a false revenue report if event semantics are wrong. ‘Payment-related’ is not a money state. An authorization can later fail capture. A subscription activation can carry an amount but represent lifecycle, not a new charge. A refund reduces recognized money rather than creating a negative-looking new sale. An unsupported event should be retained for audit but excluded from revenue until its meaning is implemented deliberately.

The original contribution behind this checklist is Metrivo's inspectable cross-provider contract fixture. It asserts that a Stripe PaymentIntent success maps to settled minor-unit money, that Stripe invoice recurrence needs explicit subscription evidence, that partial and full refunds change recognized amount without becoming new revenue, and that Razorpay authorization differs from capture. It also keeps subscription lifecycle and unknown events outside settled revenue. These are testable distinctions, not a claim that all providers use identical names.

Amounts need their own checks: provider-native minor units, currency code, zero-decimal currencies where supported, refunded amount, dispute amount, fee or net fields when available, and the relationship between a renewal invoice and its subscription. Reject or quarantine a money event when the required amount or currency evidence is missing. Guessing a currency exponent or treating any non-zero field as revenue can produce a clean dashboard that is financially wrong.

Provider-aware money events in Metrivo's current contract
ProviderCanonical settled-money exampleImportant audit distinction
Stripepayment_intent.succeeded; invoice.paid for supported renewal evidenceAuthorization, lifecycle, refund, and unsupported events are not new revenue
Razorpaypayment.captured or supported order-paid evidencepayment.authorized is not settlement; event order can vary
Dodo Paymentspayment.succeededSubscription lifecycle events remain lifecycle, not fallback paid rows
Paddletransaction.completedUse provider occurrence time; subscription lifecycle alone is not settlement
Lemon SqueezySupported order or subscription-payment success eventsSigned webhook evidence is supported; independent provider-side reconciliation is not currently claimed
Polarorder.paidorder.created and order.updated are excluded to avoid counting the same order twice

7. Prove recovery after retries stop

Concise answer

Make failed receipts visible, replay safe, and reconcilable; provider retries are a transport aid, not your complete recovery plan.

Every provider has a retry contract, and those contracts differ. Stripe documents automatic live-mode retries and manual resend tools. Paddle exposes notification status, delivery logs, and replay. Razorpay retries failed delivery under its policy. Dodo publishes an exponential retry schedule. Lemon Squeezy retries failed events a limited number of times and lets users resend recent webhooks. Your runbook should link to the current provider rule because retry counts and retention windows can change.

The audit should create a controlled outage, watch the provider mark the delivery failed or pending, restore the endpoint, and prove the event finishes once. Next, exhaust or bypass automatic retries in a safe test environment and use the supported manual replay path. The receipt must retain the original provider identity so a replay does not become a new payment. Operators need a searchable failure reason, attempt history, last safe state, and a clear instruction for whether replay is allowed.

Reconciliation closes a different gap: the event that never became a local receipt. Compare provider-side objects for a bounded time window with local canonical records, using stable object identities and currency-aware amounts. A signed webhook proves that a particular request was authentic; it cannot prove that every relevant provider event arrived. Metrivo currently distinguishes these capabilities: Stripe, Razorpay, Dodo, and Paddle support independent reconciliation paths, while Lemon Squeezy and Polar are represented as signed-webhook evidence without a claim of independent reconciliation. That limitation should remain visible in any audit result.

A reproducible test plan you can run before release

Concise answer

Use provider-generated fixtures where possible, assert final business state, and save the evidence for each failure mode—not just the HTTP response.

Create one fixture per event your business uses, captured from the provider's supported test environment or simulator and scrubbed of sensitive data. Store the raw request bytes when signature tests need them, along with the exact headers and webhook version. Hand-written JSON is useful for malformed-input tests, but it can drift away from the real provider contract and accidentally prove a payload the provider never sends.

Run the matrix below for every provider before launch and whenever you change routing, body parsing, signature libraries, queueing, database uniqueness, event mapping, billing access, refunds, or subscription state. The expected result should name the final payment, subscription, entitlement, email, commission, and audit-log effect. ‘Returns 200’ is never the only assertion for a money event.

Minimum payment webhook release matrix
ScenarioInputExpected evidence
Authentic happy pathProvider-generated signed eventOne receipt, one supported canonical effect, searchable identities
Forged requestMissing, wrong, expired, or altered signatureRejected before business effects; safe failure record
Exact replaySame provider event delivered againSuccess or safe acknowledgement; zero duplicate effects
Concurrent duplicateSame event delivered to two workersAtomic claim; one effect across every downstream action
Temporary outageFailure before durable receiptProvider retry remains possible; no partial accepted state
Worker crashFailure after receipt and before completionPending or failed receipt can resume safely
Out-of-order pairNewer terminal event followed by older eventFinal state does not roll backward
Unsupported eventAuthentic but unimplemented event typeRetained or logged; excluded from settled revenue
Refund or disputePartial, full, and repeated negative eventsRecognized amount changes once under explicit policy
Missing-event reconciliationProvider object absent locallyGap is surfaced where provider read access exists

How to review the evidence without fooling yourself

Concise answer

Classify each checkpoint as verified, inferred, or unknown, and do not let one strong layer hide a gap in another.

A delivery log marked 200 verifies that the provider received an accepted HTTP response. It does not, by itself, verify durable storage, a successful worker, correct revenue mapping, entitlement state, or reconciliation. A local payment row verifies that some write happened. It does not prove the request was authentic, that duplicates are harmless, or that another provider payment is missing. Keep each claim attached to the evidence that actually supports it.

Use three labels. Verified means a current test or persisted record directly proves the statement. Inferred means multiple records support a likely explanation but a join or provider fact is missing. Unknown means the evidence is insufficient. Unknown is a useful audit result: it points to the missing receipt, identifier, timestamp, payload field, or provider read path that should be fixed before a financial decision is made.

Avoid invented service-level targets. This article does not prescribe a universal acceptable loss rate, retry count, acknowledgement latency, or reconciliation interval. Provider deadlines are documented per platform, and business risk differs by payment volume and entitlement model. Set your internal target from the strictest provider contract, the cost of delayed access or misstated revenue, and measurements from your own system—not from an unsupported benchmark.

Where Metrivo fits—and where it does not

Concise answer

Metrivo connects accepted payment evidence to traffic and funnel context, while keeping provider capability and attribution confidence visible; it is not a payment gateway or a substitute for your handler tests.

Metrivo's payment verification documentation explains the trust boundary: provider integrations become active only after a valid signed webhook is accepted, and unmatched payments remain visible rather than being forced into an acquisition source. Its Revenue Assurance surface adds canonical money states and provider-side comparison where current provider capability supports it. That makes the output useful for the founder question: which revenue path is incomplete, and what evidence should be repaired first?

Metrivo does not move money, replace the provider dashboard, guarantee that hidden source influence is known, or make an unsafe webhook handler safe. Your application still owns entitlement changes, order fulfillment, billing emails, and provider-specific error handling. Lemon Squeezy and Polar currently have signed-webhook attribution and lifecycle evidence in Metrivo, but they are not represented as independently reconciled providers. Manual Payment API events are authenticated compatibility input, not provider-signed webhook evidence.

Use the existing provider guides for implementation details: Stripe setup, Paddle setup, Razorpay setup, Dodo Payments setup, Lemon Squeezy setup, and Polar setup. Use attribution confidence to keep source matching separate from payment truth, and monitoring to review operational failures. The article gives you the audit contract; those pages give you the Metrivo-specific path.

The one-page audit record

Concise answer

Finish with a dated record of scope, fixtures, results, gaps, owners, and the next verification date so the audit remains operational.

Write the provider account and mode, destination identifier, webhook version, subscribed events, deployed commit, test environment, and verification date. For every scenario, link the provider delivery, local receipt, processing record, canonical payment or lifecycle row, and downstream effect. Record the exact reason for any unknown. A concise evidence record is more useful than a screenshot collage because another engineer can reproduce it after a deployment or incident.

Give every failed or unknown control one owner and one next action. Typical actions are moving raw-body verification ahead of parsing, adding a database uniqueness constraint, separating receipt from processing, guarding state transitions with provider time, excluding a lifecycle event from revenue, adding failed-receipt alerts, or enabling a provider read path for reconciliation. Retest the narrow failure first, then rerun the full matrix before calling the pipeline healthy.

If you want Metrivo to help connect the payment side of this audit to the source, landing page, funnel, and checkout path, use the existing Add my website flow below. It starts real onboarding for one website and one payment path. The separate demo remains a no-signup seeded product sample, not a live audit of your URL or customer data.

Frequently asked questions

What should a payment webhook audit include?

It should include destination and mode checks, signature verification on the raw request, durable receipt before acknowledgement, idempotency under sequential and concurrent duplicates, out-of-order state tests, explicit payment-state mapping, recovery after failures, provider-side reconciliation where supported, and an evidence record for every result.

Does a 200 response prove that a payment webhook worked?

No. A 200 response proves only that the receiver acknowledged delivery under the provider's contract. You still need evidence that the event was stored, processed once, mapped to the correct money or lifecycle state, applied to downstream systems, and compared with provider truth where reconciliation is available.

How do I test webhook idempotency?

Deliver the exact same authentic event twice sequentially and concurrently, then repeat after a worker crash and a manual replay. Assert every business effect, not only the payment row: entitlements, emails, commissions, jobs, metrics, and audit records must not duplicate. Enforce uniqueness atomically in durable storage using the provider's stable identity.

How should I handle out-of-order payment events?

Use the provider's occurrence timestamp, object version, or a provider-specific valid-state transition rule instead of request arrival time. Test reversed event pairs and confirm an older event cannot overwrite a newer terminal state. When the payload cannot establish the latest state safely, retrieve current provider state if that capability exists.

Are payment webhooks the source of truth?

They are signed delivery evidence and often the primary notification path, but a receipt cannot prove that no relevant event was missed. Independent reconciliation requires a separate provider-side read or export compared with local canonical records. If that path is unavailable, report signed-webhook evidence honestly and keep completeness unknown rather than claiming full reconciliation.

Which Metrivo providers support independent reconciliation?

In Metrivo's current code-backed capability matrix, Stripe, Razorpay, Dodo Payments, and Paddle support independent reconciliation paths. Lemon Squeezy and Polar support signed attribution and lifecycle webhooks but are not currently marked as independently reconciled. The Manual Payment API is an authenticated compatibility path, not provider-signed evidence.