Skip to content

PayU payment provider

Status: integration candidate. Razorpay remains the default and rollback provider.

Safety boundary

PayU uses Hosted Checkout. VayzoMart never collects card data and never sends the merchant salt to a browser or mobile client. The API creates the transaction ID and request hash. A browser return is not proof of payment: the API validates PayU's reverse hash and calls verify_payment before the shared checkout finalizer can create an order or mark it paid.

Both providers converge on the existing atomic inventory-reservation finalizer. Its database conditional update elects exactly one winner when a browser return, webhook, or duplicate delivery races another request. PaymentTransaction.Provider identifies the provider while settlement, invoice, tax, cancellation, and internal refund accounting remain provider-neutral.

Configuration

Razorpay is the safe default:

PaymentGateway__Provider=Razorpay

Staging PayU activation requires all of the following runtime secrets/settings:

PaymentGateway__Provider=PayU
PayU__Enabled=true
PayU__Environment=Test
PayU__MerchantKey=<PayU TEST merchant key>
PayU__MerchantSalt=<PayU TEST salt>
PayU__WebhookEnabled=true
PayU__RefundExecutionEnabled=false
PayU__ReconciliationEnabled=false
PayU__RefundCallbackUrl=https://<api-host>/api/webhooks/payu/refund
PayU__FrontendReturnUrl=https://<frontend-host>/checkout/payment-callback

Staging startup rejects non-TEST PayU endpoints. Production startup rejects TEST endpoints and requires PayU:Environment=Live. Callback URLs must be real HTTPS URLs, and PayU:FrontendReturnUrl must point at /checkout/payment-callback (the page that asks /api/payment/status for the opaque txnid); startup rejects any other path. Razorpay credentials stay configured while PayU is selected so rollback does not require rebuilding the service.

Never commit merchant keys, salts, Razorpay secrets, webhook secrets, or database credentials.

Webhooks and reconciliation

POST /api/webhooks/payu is anonymous to application JWT authentication but accepts traffic only when PayU and its webhook switch are enabled. It validates the provider response hash before recording both an exact payload fingerprint and a provider-qualified semantic event key. Equivalent deliveries with reordered or additional form fields return success without re-running business logic. Logs include transaction identifiers and machine-readable outcomes but not raw payloads, hashes, credentials, or salts.

Provider identity is persisted on the inventory reservation and checkout-idempotency record, so a provider switch cannot replay or finalize another provider's same-looking identifier. PayU verification outcomes are durable (Pending, Unknown, Failed, or Verified). Pending and unknown reservations are excluded from ordinary expiry release and are re-verified by the payment reconciliation worker after restarts. A verified capture that arrives after inventory was released creates one durable, provider-qualified refund intent instead of silently losing the payment.

PayU refund requests use the internal refund transaction's deterministic merchant reference (at most 23 characters; longer keys become a deterministic digest). Dispatch continues to use the existing database claim/lease rules. A PayU refund is never re-sent once its outcome is ambiguous: a timeout, HTTP 429/5xx, transport failure, unusable response, a "token already used / request pending" answer, or error_code 102 without a request id moves the attempt straight to UnknownOutcome (Razorpay keeps its transient retry). error_code 102 or a duplicate-token answer that does carry a request id is recorded as Submitted. Only an explicit status=0 rejection is a definitive failure. Submitted and UnknownOutcome PayU attempts are resolved only by check_action_status (by request id when known, otherwise by PayU payment id plus merchant token); the provider's own request_id is recorded, never a locally invented one.

A verify_payment response without an explicit per-transaction "Not Found" record (invalid hash, wrong key, top-level error, unexpected shape) is a lookup failure, never "not initiated", so it can never authorize releasing inventory. A browser return whose form fails key or hash validation is sent back (303) to the callback page without any reference or state; the server-to-server webhook keeps rejecting it outright.

Refund execution and reconciliation are separate kill switches and default to false. Enable refund execution only after TEST refund and check_action_status certification; startup rejects refund execution without reconciliation. Checkout and webhook activation do not implicitly activate money movement.

Rollback

Set PaymentGateway__Provider=Razorpay and restart through the normal controlled deployment process. Do not delete PayU payment, webhook, or refund records. Transactions already initiated on PayU must continue to accept valid callbacks/webhooks and must be reconciled with PayU even after new checkout creation returns to Razorpay.

No production activation is authorized by this document or by the implementation tranche.