A Frappe app that connects PayU India to your bench — capture payments via hosted checkout, share Payment Links with customers, log signed webhooks, and issue refunds, all from the desk.
This integration was built during the BuildWithHussain.com/school integrations training — a hands-on cohort that teaches building production-grade Frappe integrations end-to-end.
- Getting Started
- Configuring Settings
- Features
- Code Examples
- Webhook URL & Security
- PayU Documentation Reference
cd $PATH_TO_YOUR_BENCH
bench get-app https://github.com/BuildWithHussain/payu_frappe --branch develop
bench --site <your-site> install-app payuYou need a PayU India merchant account. Sign up at payu.in, then collect:
| Field | Where to find it |
|---|---|
| MID | PayU Dashboard → Settings → Profile (Merchant ID) |
| Key | PayU Dashboard → Settings → My Account → API Configuration |
| Salt | Same screen as Key. Salt v2 (SHA-512) is what this app uses. |
| Client ID | PayU OAuth credentials — Settings → API Configuration → OAuth (used for Payment Links). |
| Client Secret | Same screen as Client ID. |
For sandbox testing, switch your PayU dashboard to Test Mode — it surfaces a separate set of test MID/Key/Salt without needing to contact support. Plug those into PayU Settings here and tick the Test Mode checkbox; all API calls then route to PayU's UAT endpoints (apitest.payu.in, uatoneapi.payu.in, test.payu.in).
Open the desk → PayU Settings and fill in the credentials from above. Tick Test Mode while you're in development.
Salt and Client Secret are stored encrypted via Frappe's password manager.
A single-doc settings page. Holds the credentials and the Test Mode toggle that routes all outgoing API calls between PayU's UAT and live endpoints.
Each row represents a hosted-checkout payment kicked off from your site. Created when you call initiate_checkout — the app gets back a checkoutUrl and saves it on the doc, so the customer can be redirected to PayU.
The form keeps the mihpayid (PayU's internal payment id), the local status (Pending / Success / Failure / Cancelled), the amount and currency, and a child table of refunds against the transaction.
Buttons on the form:
- Sync from PayU — re-queries the v3 transaction API and updates the local status / mihpayid.
- Refund — opens the refund dialog (only shown when status is
Success, mihpayid exists, and refundable balance > 0). - Sync Refund Status — polls PayU for any refund rows currently in
Queuedand updates them.
Stand-alone payment links you share with customers (e.g. via email/SMS). On before_insert the app calls PayU's payment-links API and stores the returned URL. Webhooks update the link's status to Paid once the customer pays.
Every webhook hit from PayU lands here. The flow:
- Verify signature — the request's
hashis recomputed with your Salt using PayU's reverse-hash formula (sha512(SALT|status||||||udf5|udf4|udf3|udf2|udf1|email|firstname|productinfo|amount|txnid|key)). Tampered or unsigned payloads return401. - Persist — the full payload is stored as a
PayU Webhook Logrow withstatus=Pending. - Enqueue — processing happens in a background RQ job (
shortqueue) so PayU gets a fast200. - Update — on success the row flips to
Processedwith aprocessed_attimestamp; on failure it flips toFailedwith the traceback in theerrorfield.
A Reprocess button on Failed (and Pending) rows re-enqueues the job — useful when a downstream doctype was missing the first time around.
The Refund button on a successful transaction opens this dialog:
It shows the original amount, what's already been refunded, and the remaining refundable balance. You can issue a full or partial refund. The app:
- Generates a
token_id(idempotency key for PayU) - Hashes
key|cancel_refund_transaction|mihpayid|salt(PayU's postservice hash format) - POSTs to PayU's refund endpoint
- Adds a row to the
Refundschild table withQueuedstatus and therequest_id
Click Sync Refund Status later to pull the final state (Success / Failed) from PayU's check_action_status API.
The initiate_checkout whitelisted method creates a PayU Transaction record, calls PayU's v2 payments API, and returns a checkout_url you redirect the customer to.
From Python (server-side):
import frappe
result = frappe.call(
"payu.api.initiate_checkout",
product_name="cohort-fee", # a Website Item with a price
qty=1,
)
# result == {"checkout_url": "https://apitest.payu.in/public/#/..."}From the browser (client-side):
frappe.call({
method: "payu.api.initiate_checkout",
args: { product_name: "cohort-fee", qty: 1 },
}).then((r) => {
window.location.href = r.message.checkout_url;
});import frappe
link = frappe.get_doc({
"doctype": "PayU Payment Link",
"description": "Cohort Fee — May batch",
"amount": 1499,
"customer_name": "Jane Doe",
"customer_email": "jane@example.com",
"customer_phone": "9876543210",
}).insert()
print(link.url) # https://pay.payu.in/...txn = frappe.get_doc("PayU Transaction", "7l8fh2lgho")
txn.refund(amount=500) # partial refund of ₹500
# Later, pull the latest status from PayU:
txn.sync_refund_statuses()The refund method validates that the transaction is Success, has a mihpayid, and that the requested amount fits within the remaining refundable balance — it raises a clear frappe.throw otherwise.
Configure this URL in your PayU dashboard's webhook settings:
https://<your-site>/api/method/payu.api.webhook
The endpoint:
- Verifies the SHA-512 reverse hash against your Salt before doing anything else
- Rejects with
frappe.AuthenticationErrorif the hash is missing or wrong - Logs every accepted hit to PayU Webhook Log for auditability
Never expose your Salt or Client Secret in client-side code.
| Topic | Link |
|---|---|
| API overview | https://docs.payu.in/ |
| Webhooks | https://docs.payu.in/docs/webhooks |
| Hash request & response | https://docs.payu.in/docs/hashing-request-and-response |
| Generate hash (merchant hosted) | https://docs.payu.in/docs/generate-hash-merchant-hosted |
| Server-to-server (v2/v3) integration | https://docs.payu.in/docs/integrate-with-s2s |
| Payment Links API | https://docs.payu.in/docs/create-payment-link |
| Refunds (postservice) | https://docs.payu.in/docs/refund-transaction-api |
| OAuth tokens | https://docs.payu.in/docs/get-oauth-token |
Pre-commit (ruff, eslint, prettier, pyupgrade):
cd apps/payu
pre-commit installMIT






