A duplicate payment webhook should produce one intended fulfillment effect for the same verified transaction. For PayRequest payment.succeeded events, verify the raw-body signature first, then use the documented data.id transaction identifier in a durable processing key. Repeating an HTTP notification must not create another delivery or service credit.
This checklist is for developers connecting PayRequest payments to their own fulfillment system. It is a receiver design and acceptance protocol, not a claim that an external action becomes exactly-once merely because you added a database row.
Verify the contract before choosing a key
The PayRequest webhook documentation describes HTTPS POST delivery, the X-PayRequest-Signature header and HMAC-SHA256 over the raw request body. Validate that signature with a server-side secret and a timing-safe comparison before accepting an event. Validate the header format and length too; some timing-safe functions reject unequal-length inputs. Parse and validate the payload only after preserving the bytes needed for verification.
For payment.succeeded, data.id is the transaction ID. The published payment payload does not document a separate top-level event ID. Do not copy another provider's event.id recipe without checking this contract. Keep secrets out of browser code and avoid logging complete customer payloads unnecessarily.
Identify the payment, not the shared link
An illustrative key is integration-account / payment.succeeded / data.id. Include the account scope when your receiver handles multiple merchant accounts. This is your receiver's key design, not a field PayRequest generates for you.
A payment_link_id identifies a link that may receive multiple legitimate payments. Customer email, amount and description are also unsuitable as the sole key. Two customers can pay the same amount; one customer can make two separate valid purchases. A repeated data.id and a new data.id on the same link require different treatment.
Map the transaction to a real order or entitlement using a controlled reference you established in the integration. A success event alone does not define which package, service period or quantity to fulfill. Quarantine an unrecognized reference instead of delivering an arbitrary product.
Compare the amount and currency with the intended order before authorizing delivery. Unexpected values need review rather than automatic fulfillment.
Make acceptance durable before acknowledging
Use a unique constraint for the processing key and an atomic transaction to persist the accepted payment and one pending fulfillment task. A durable outbox separates receipt from slow work. Return a successful acknowledgement only after durable acceptance; a known duplicate should not create another task.
PayRequest accepts a 2xx acknowledgement. Its docs specify five attempts for security-deposit delivery, but refer to the existing queue policy for payments without documenting that count. Do not assume the deposit retry schedule applies to payment events, or use a supposed maximum retry window as your deduplication policy.
A worker still needs recovery controls. If an external delivery succeeds and the worker crashes before recording completion, retrying may repeat the external action. Use the downstream system's supported idempotency key or reconcile the external result before repeating it. The local ledger does not guarantee exactly-once behavior across systems.
Run this original replay matrix
Use an isolated receiver and illustrative signed fixtures with no real fulfillment. Sign test bodies with a test secret; changing the body requires a new signature. These are proposed acceptance tests, not tests performed on a production customer account.
| Test | Input | Required receiver outcome |
|---|---|---|
| Replay | Same valid transaction twice | One accepted payment and one task |
| Concurrent delivery | Two valid copies arrive together | Unique constraint prevents two tasks |
| Legitimate second sale | Same link, different transaction ID | Two separate payments and tasks |
| Forged body | Altered body with old signature | Rejected before fulfillment |
| Worker interruption | Crash after task persistence | Recover pending work without silently dropping it |
Also test unknown references and malformed signature lengths. Count durable records and external effects separately. A 200 response is evidence of your acknowledgement, not of completed delivery.
Reconcile the payment and the effect
Retain the transaction ID, account scope, processing state, task identifier and downstream result reference. Investigate paid transactions with pending fulfillment; replay through the same controlled path rather than sending a manual second delivery that bypasses the ledger.
Start with PayRequest API documentation and map one payment.succeeded event to one fulfillment task. Use Payment Matching for the financial reconciliation context; payment reconciliation and delivery completion remain separate records.
Editorial note: AI assisted with this article and its illustrative cover. The published contract was checked on 4 October 2026. The processing key, matrix and architecture are original recommendations, not a production integration test or delivery guarantee.


