[AVAILABLE],
packages/shared/src/domain/settlement-confirmation.ts, packages/api/src/webhooks/RazorpaySettlementProcessor.ts, packages/crypto/src/SettlementConfirmationCrypto.ts. Scoped to the Razorpay refund lifecycle today, not a general-purpose settlement primitive for other connectors.What it is
An Execution Trust Record records that an action was authorized and executed. For most connectors that’s the end of the story, the connector’s response is the outcome. A refund is different: Razorpay accepting a refund-create call doesn’t mean the refund actually settled, it means Razorpay is going to try. What actually happened arrives later, asynchronously, as a webhook.SettlementConfirmation is the signed
record that closes that gap: it’s appended to the original Trust Record once the outcome is
independently confirmed, never replacing or mutating anything that came before it.
Fetch-verify, not trust-the-webhook
This is the load-bearing design decision. A webhook is treated strictly as a doorbell, never a delivery.RazorpaySettlementProcessor never derives status from the webhook payload’s
own claimed event type (refund.processed vs refund.failed), it uses the webhook only to
learn that something changed, then independently calls Razorpay’s own refund-fetch API to
learn what actually changed:
refund.processed cannot produce a SETTLED
confirmation on its own, the fetched status from Razorpay always wins.
The full lifecycle
What triggers it, step by step
- The settlement poller drains the webhook event store on an interval
(
RAZORPAY_SETTLEMENT_POLL_INTERVAL_MS, default15000ms), acting only onrefund.processedandrefund.failedevents, everything else is stored but ignored. - It extracts the Razorpay refund id and the original
businessTransactionId(carried in the refund’s ownnotes["parmana_txn"]field, set at refund-creation time), then looks up the matching Execution Trust Record. - If the Trust Record isn’t found yet (a webhook can arrive before the local record is fully committed), the event is parked with bounded retry, default five attempts, not dropped and not treated as an error.
- Durable idempotency check. If a
SettlementConfirmationalready references this exactwebhookEventId, processing stops, nothing is signed twice for the same delivery. - Fetch-verify. The processor calls Razorpay’s
razorpay:refund-fetchcapability against the real refund, never the webhook’s own payload, to learn the authoritative status. A fetch failure parks the event for retry rather than failing the confirmation. - Sign.
SettlementConfirmationCryptocomputes a hash over the confirmation fields, signs it with the same local PEM key and algorithm a Receipt is signed with (Ed25519 in the current single-provider configuration, see Choose a signature provider), and appends the result to the Trust Record. Append-only, the original record is never mutated.
Reading a confirmation back
settlementConfirmations array, every confirmation ever appended, not just
the latest.
Example
schemas/common/settlement-confirmation.schema.json’s own example.
receiptId is optional, present when the original approval produced one.
Key custody, stated plainly
The signing key is a local PEM file read byFileKeyProvider, the same mechanism and the
same exposure noted on Security. No KMS, HSM, or cloud key vault
integration exists for this signature, that’s tracked as [ROADMAP] on the same page.
What’s proven where
The full lifecycle above (refund, mocked webhook, poll,SETTLED confirmation, read back via
GET /verification) runs against MockRazorpayServer on every npm test, no gate required.
The identical lifecycle against real Razorpay infrastructure, a real webhook delivery and a
real fetch-verify round trip, has been run once, manually, in both Razorpay test mode and live
mode. See Razorpay for the exact scope
of each claim.
Next
Verify Razorpay webhooks
How the webhook that triggers this gets authenticated in the first place.
Razorpay
The connector and policy this lifecycle closes out.