Partner Payments UPI Intent Integration
Partner Payment UPI Intent enables partners to initiate UPI payments through a server-to-server (S2S) flow that directly invokes UPI apps on the customer's mobile device. Unlike hosted checkout flows that redirect customers to a web page, UPI Intent provides a seamless, native mobile payment experience.
When you initiate a UPI Intent payment, PayU returns an intentURIData string that contains pre-filled UPI payment details. Your mobile application uses this data to launch the customer's preferred UPI app (Google Pay, PhonePe, BHIM, Paytm, etc.) with transaction details already populated. The customer simply authenticates using their UPI PIN, and the payment is complete.
Key Benefits:
- Native mobile experience — Payments happen within UPI apps customers already trust
- Faster checkout — No form filling, no card details entry
- Higher success rates — Reduced friction leads to better conversion
- Real-time confirmation — Instant payment status updates via webhooks
This integration is ideal for:
- Mobile-first applications with high UPI payment volume
- Quick checkout flows for ride-hailing, food delivery, e-commerce apps
- In-app purchases requiring seamless payment experiences
- QR code alternatives for merchant-initiated UPI flows
How It Works
The Partner Payment UPI Intent flow follows these steps:
-
Get Access Token — Obtain an access token with scope:
hub_session -
Initiate UPI Intent Payment — POST a payment request with
txn_s2s_flow=4and customer device details (s2s_client_ip,s2s_device_info) -
Receive intentURIData — PayU returns a UPI deep link string with pre-filled payment parameters
-
Invoke UPI App — Use the intentURIData to launch the customer's UPI app on their mobile device
-
Customer Authenticates — Customer enters their UPI PIN in the UPI app to authorize payment
-
Receive Webhook — PayU sends payment status notification to your configured webhook URL
-
Verify Payment — Call the Verify Payment API to confirm final transaction status
Prerequisites
Before you begin, ensure you have:
- Partner OAuth Application registered with PayU with the above scopes enabled
- OAuth Credentials:
client_idandclient_secret - Merchant Credentials:
merchant_id(PayU merchant ID) andreseller_id(partner UUID) - S2S Flow Enabled — Your account must be enabled for
txn_s2s_flow=4(contact PayU if not enabled) - Partner Webhook URLs configured (
partner_webhook_success,partner_webhook_failure,partner_webhook_cancelled) - Ability to Capture:
- Customer IP address (
s2s_client_ip) - Device user-agent string (
s2s_device_info)
- Customer IP address (
- Test Environment Access to
https://test-partnerapilayer.payu.in
Step 1: Get the Access Token
Step 1.1: Receiving the auth_code on the redirect URI
The auth_code is received on the configured redirect URI.
Step 1.2: Validate this auth_code
Validate this auth_code using the Validate Auth Code and Client API.
You will receive an access_token.
Request Parameters
| Parameter | Required | Description | Example value |
|---|---|---|---|
client_id | Yes | Client identifier. | {{client_id}} |
client_secret | Yes | Client secret code. | {{client_secret}} |
grant_type | Yes | Grant type used to obtain an access token in this flow. Must be authorization_code. | authorization_code |
code | Yes | Authorization code received on the redirect URI. | {{authorization_code}} |
redirect_uri | Yes | Redirect URL associated with the authorization request. It must match the redirect URI used for the authorization request. | {{redirect_uri}} |
Samp[e request
curl --location '{{accounts_base_url}}/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id={{client_id}}' \
--data-urlencode 'client_secret={{client_secret}}' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code={{authorization_code}}' \
--data-urlencode 'redirect_uri={{redirect_uri}}'Sample response
{
"access_token": "{{access_token}}",
"token_type": "Bearer",
"expires_in": {{expires_in}},
"refresh_token": "{{refresh_token}}",
"scope": "{{scope}}",
"created_at": {{created_at}},
"user_uuid": "{USER_UUID}"
}Step 1.3: Use the access_token
Send the access token as a bearer token when calling Partner Integration APIs.
Step 2: Initiate UPI Intent Payment
Step 2.1: Prepare Request Parameters
Endpoint URLs:
| Environment | URL |
|---|---|
| Test | https://test-partnerapilayer.payu.in/apilayer/partner/payments |
| Production | https://api.payu.in/partner/payments |
HTTP Method: POST
Headers:
Authorization: Bearer <your_access_token>
Content-Type: application/json
Request Body Parameters:
Mandatory Parameters
| Parameter | Type & Description | Example |
|---|---|---|
| txnid | string - Unique transaction ID generated by the partner. | UPIINT20240315001 |
| amount | string - Transaction amount in decimal format. | 500.00 |
| productinfo | string - Product or service description. | UPI Payment for Order #12345 |
| phone | string - Customer phone number with country code (10 digits). | 919876543210 |
| merchant_id | integer - PayU merchant ID. | 8739528 |
| reseller_id | string - Partner/reseller UUID. | 11ee-0e7e-5403fde2-9523-0a696b110fde |
| txn_s2s_flow | string - Server-to-server flow identifier. Must be 4 for UPI Intent. | 4 |
| s2s_client_ip | string - Customer's IP address. | 157.240.22.9 |
| s2s_device_info | string - Customer's device user-agent string. | Mozilla/5.0 (Linux; Android 10) |
| hash | string - SHA-512 hash for request authentication, encoded as a lowercase hex string. Computed as SHA-512(merchant_id|txnid|amount|productinfo|firstname|email|udf1|udf2|udf3|udf4|udf5||||||client_secret) | a3f7c92e1b... |
Optional Parameters
| Parameter | Type & Description | Example |
|---|---|---|
| firstname | string - Customer's first name. | Rajesh |
| string - Customer's email address. | [email protected] | |
| udf1 | string - User-defined field 1 for custom data. | custom_value_1 |
| udf2 | string - User-defined field 2 for custom data. | custom_value_2 |
| udf3 | string - User-defined field 3 for custom data. | custom_value_3 |
| udf4 | string - User-defined field 4 for custom data. | custom_value_4 |
| udf5 | string - User-defined field 5, often used for partner or channel ID. | partner_channel_001 |
Step 2.2: Generate Payment Request Hash
Hash Formula:
merchant_id|txnid|amount|productinfo|firstname|email|udf1|udf2|udf3|udf4|udf5||||||client_secret
Sample Hash Generation Code:
Python:
import hashlib
def generate_upi_intent_hash(merchant_id, txnid, amount, productinfo, firstname, email, udf1, udf2, udf3, udf4, udf5, client_secret):
hash_string = f"{merchant_id}|{txnid}|{amount}|{productinfo}|{firstname}|{email}|{udf1}|{udf2}|{udf3}|{udf4}|{udf5}||||||{client_secret}"
return hashlib.sha512(hash_string.encode('utf-8')).hexdigest()
# Example
payment_hash = generate_upi_intent_hash(
merchant_id=8739528,
txnid="UPIINT20240315001",
amount="500.00",
productinfo="UPI Payment for Order #12345",
firstname="Rajesh",
email="[email protected]",
udf1="",
udf2="",
udf3="",
udf4="",
udf5="partner_channel_001",
client_secret="your_client_secret_here"
)
print(f"Payment Hash: {payment_hash}")Java:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class UPIIntentHashGenerator {
public static String generateHash(
int merchantId, String txnid, String amount, String productinfo,
String firstname, String email, String udf1, String udf2,
String udf3, String udf4, String udf5, String clientSecret
) throws NoSuchAlgorithmException {
String hashString = merchantId + "|" + txnid + "|" + amount + "|" +
productinfo + "|" + firstname + "|" + email + "|" +
udf1 + "|" + udf2 + "|" + udf3 + "|" + udf4 + "|" +
udf5 + "||||||" + clientSecret;
MessageDigest md = MessageDigest.getInstance("SHA-512");
byte[] hashBytes = md.digest(hashString.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
}PHP:
<?php
function generateUPIIntentHash($merchantId, $txnid, $amount, $productinfo,
$firstname, $email, $udf1, $udf2, $udf3,
$udf4, $udf5, $clientSecret) {
$hashString = $merchantId . "|" . $txnid . "|" . $amount . "|" .
$productinfo . "|" . $firstname . "|" . $email . "|" .
$udf1 . "|" . $udf2 . "|" . $udf3 . "|" . $udf4 . "|" .
$udf5 . "||||||" . $clientSecret;
return hash('sha512', $hashString);
}
$hash = generateUPIIntentHash(
8739528,
"UPIINT20240315001",
"500.00",
"UPI Payment for Order #12345",
"Rajesh",
"[email protected]",
"",
"",
"",
"",
"partner_channel_001",
"your_client_secret_here"
);
echo "Payment Hash: " . $hash;
?>Step 2.3: POST the Payment Request
Sample Request (cURL):
curl --location 'https://test-partnerapilayer.payu.in/apilayer/partner/payments' \
--header 'Authorization: Bearer your_access_token_here' \
--header 'Content-Type: application/json' \
--data '{
"txnid": "UPIINT20240315001",
"amount": "500.00",
"productinfo": "UPI Payment for Order #12345",
"firstname": "Rajesh",
"email": "[email protected]",
"phone": "919876543210",
"merchant_id": 8739528,
"reseller_id": "11ee-0e7e-5403fde2-9523-0a696b110fde",
"txn_s2s_flow": "4",
"s2s_client_ip": "157.240.22.9",
"s2s_device_info": "Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36",
"udf5": "partner_channel_001",
"hash": "computed_sha512_hash_here"
}'Sample Request (Python):
import requests
import json
url = "https://test-partnerapilayer.payu.in/apilayer/partner/payments"
headers = {
'Authorization': 'Bearer your_access_token_here',
'Content-Type': 'application/json'
}
payload = {
"txnid": "UPIINT20240315001",
"amount": "500.00",
"productinfo": "UPI Payment for Order #12345",
"firstname": "Rajesh",
"email": "[email protected]",
"phone": "919876543210",
"merchant_id": 8739528,
"reseller_id": "11ee-0e7e-5403fde2-9523-0a696b110fde",
"txn_s2s_flow": "4",
"s2s_client_ip": "157.240.22.9",
"s2s_device_info": "Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36",
"udf5": "partner_channel_001",
"hash": "computed_sha512_hash_here"
}
try:
response = requests.post(url, headers=headers, data=json.dumps(payload))
print(f"Status Code: {response.status_code}")
print(f"Response: {response.text}")
except Exception as e:
print(f"Error: {str(e)}")Sample Request (Java):
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class CreateUPIIntentPayment {
public static void main(String[] args) throws Exception {
String url = "https://test-partnerapilayer.payu.in/apilayer/partner/payments";
String payload = "{\"txnid\":\"UPIINT20240315001\",\"amount\":\"500.00\",\"productinfo\":\"UPI Payment for Order #12345\",\"firstname\":\"Rajesh\",\"email\":\"[email protected]\",\"phone\":\"919876543210\",\"merchant_id\":8739528,\"reseller_id\":\"11ee-0e7e-5403fde2-9523-0a696b110fde\",\"txn_s2s_flow\":\"4\",\"s2s_client_ip\":\"157.240.22.9\",\"s2s_device_info\":\"Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36\",\"udf5\":\"partner_channel_001\",\"hash\":\"computed_sha512_hash_here\"}";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer your_access_token_here")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Status Code: " + response.statusCode());
System.out.println("Response Body: " + response.body());
}
}Sample Request (PHP):
<?php
$url = "https://test-partnerapilayer.payu.in/apilayer/partner/payments";
$headers = array(
'Authorization: Bearer your_access_token_here',
'Content-Type: application/json'
);
$payload = json_encode(array(
"txnid" => "UPIINT20240315001",
"amount" => "500.00",
"productinfo" => "UPI Payment for Order #12345",
"firstname" => "Rajesh",
"email" => "[email protected]",
"phone" => "919876543210",
"merchant_id" => 8739528,
"reseller_id" => "11ee-0e7e-5403fde2-9523-0a696b110fde",
"txn_s2s_flow" => "4",
"s2s_client_ip" => "157.240.22.9",
"s2s_device_info" => "Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36",
"udf5" => "partner_channel_001",
"hash" => "computed_sha512_hash_here"
));
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "Status Code: " . $statusCode . "\n";
echo "Response: " . $response;
?>Step 2.4: Handle Payment Response
Success Response:
{
"metaData": {
"referenceId": "11ee-0e7e-5403fde2-9523-0a696b110fde",
"txnId": "UPIINT20240315001",
"txnStatus": "pending",
"unmappedStatus": "pending",
"message": "Transaction initiated successfully",
"statusCode": "200"
},
"result": {
"paymentId": "30478359672",
"merchantName": "Your Merchant Name",
"merchantVpa": "payu@axisbank",
"amount": "500.00",
"intentURIData": "pa=payu@axisbank&pn=Your+Merchant+Name&tr=30478359672&tid=UPIINT20240315001&am=500.00&cu=INR&tn=UPIIntent",
"acsTemplate": null,
"otpPostUrl": null
}
}Key Response Fields:
| Field | Description |
|---|---|
metaData.txnStatus | Initial status (typically "pending" for UPI Intent) |
result.paymentId | PayU's internal payment ID (mihpayid) |
result.merchantVpa | Merchant's UPI VPA for this transaction |
result.intentURIData | Critical — UPI deep link string to invoke UPI apps |
result.acsTemplate | Base64-encoded HTML template (used in some flows, typically null for pure Intent) |
result.otpPostUrl | OTP submission URL (used in some flows, typically null for pure Intent) |
Invoking the UPI App:
Android (Intent URI):
// Construct UPI URI from intentURIData
String intentData = result.getString("intentURIData");
String upiUri = "upi://pay?" + intentData;
// Create Intent
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(upiUri));
// Check if any UPI app is available
PackageManager packageManager = getPackageManager();
List<ResolveInfo> activities = packageManager.queryIntentActivities(intent, 0);
if (activities.size() > 0) {
// Launch UPI app
startActivityForResult(intent, UPI_PAYMENT_REQUEST_CODE);
} else {
// No UPI app installed
showError("Please install a UPI app (Google Pay, PhonePe, etc.)");
}iOS (URL Scheme):
// Construct UPI URL from intentURIData
let intentData = result["intentURIData"] as! String
let upiUrlString = "upi://pay?\(intentData)"
if let upiUrl = URL(string: upiUrlString) {
if UIApplication.shared.canOpenURL(upiUrl) {
// Launch UPI app
UIApplication.shared.open(upiUrl, options: [:], completionHandler: nil)
} else {
// No UPI app installed
showError("Please install a UPI app")
}
}Web/Mobile Web:
For mobile web browsers, you can attempt to open the UPI deep link:
const intentData = response.result.intentURIData;
const upiUrl = `upi://pay?${intentData}`;
// Attempt to open UPI app
window.location.href = upiUrl;
// Set a timeout to show fallback if app doesn't open
setTimeout(() => {
// Show QR code or other fallback
}, 3000);Step 3: Receive Payment Notification
Step 3.1: Partner Webhook
After the customer completes (or cancels) the payment in their UPI app, PayU sends a webhook notification to your configured partner webhook URL.
Webhook Configuration:
Ensure these URLs are configured in PayU's system:
- partner_webhook_success — Called on successful payment
- partner_webhook_failure — Called on failed payment
- partner_webhook_cancelled — Called when payment is cancelled
Sample Success Webhook Payload:
{
"key": "JPM7Fg",
"txnid": "UPIINT20240315001",
"mihpayid": "30478359672",
"status": "success",
"unmappedstatus": "captured",
"mode": "UPI",
"bankcode": "INTENT",
"amount": "500.00",
"productinfo": "UPI Payment for Order #12345",
"firstname": "Rajesh",
"email": "[email protected]",
"phone": "919876543210",
"udf1": "",
"udf2": "",
"udf3": "",
"udf4": "",
"udf5": "partner_channel_001",
"merchant_id": "8739528",
"error": "No Error",
"error_Message": "No Error",
"hash": "webhook_hash_from_payu"
}UPI Intent-Specific Fields:
| Field | Value for UPI Intent |
|---|---|
mode | "UPI" |
bankcode | "INTENT" |
unmappedstatus | "captured" (success) or "failed" (failure) |
Step 3.2: Verify Webhook Hash
Always verify the webhook hash before processing the notification.
Reverse Hash Formula:
client_secret|status|||||udf5|udf4|udf3|udf2|udf1|email|firstname|productinfo|amount|txnid|merchant_id
Sample Verification Code:
Python:
import hashlib
def verify_upi_intent_webhook_hash(webhook_payload, client_secret):
status = webhook_payload.get('status', '')
udf5 = webhook_payload.get('udf5', '')
udf4 = webhook_payload.get('udf4', '')
udf3 = webhook_payload.get('udf3', '')
udf2 = webhook_payload.get('udf2', '')
udf1 = webhook_payload.get('udf1', '')
email = webhook_payload.get('email', '')
firstname = webhook_payload.get('firstname', '')
productinfo = webhook_payload.get('productinfo', '')
amount = webhook_payload.get('amount', '')
txnid = webhook_payload.get('txnid', '')
merchant_id = webhook_payload.get('merchant_id', '')
received_hash = webhook_payload.get('hash', '')
hash_string = f"{client_secret}|{status}|||||{udf5}|{udf4}|{udf3}|{udf2}|{udf1}|{email}|{firstname}|{productinfo}|{amount}|{txnid}|{merchant_id}"
computed_hash = hashlib.sha512(hash_string.encode('utf-8')).hexdigest()
return computed_hash.lower() == received_hash.lower()
# Example
is_valid = verify_upi_intent_webhook_hash(webhook_data, "your_client_secret")
if is_valid:
print("✅ Webhook verified — UPI Intent payment confirmed")
else:
print("❌ Invalid webhook hash — reject")Java:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class UPIIntentWebhookVerifier {
public static boolean verifyHash(
String status, String udf5, String udf4, String udf3, String udf2, String udf1,
String email, String firstname, String productinfo, String amount,
String txnid, String merchantId, String receivedHash, String clientSecret
) throws NoSuchAlgorithmException {
String hashString = clientSecret + "|" + status + "|||||" +
udf5 + "|" + udf4 + "|" + udf3 + "|" + udf2 + "|" + udf1 + "|" +
email + "|" + firstname + "|" + productinfo + "|" +
amount + "|" + txnid + "|" + merchantId;
MessageDigest md = MessageDigest.getInstance("SHA-512");
byte[] hashBytes = md.digest(hashString.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : hashBytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString().equalsIgnoreCase(receivedHash);
}
}Step 3.3: Process Webhook
Python Flask Webhook Handler:
from flask import Flask, request, jsonify
import hashlib
app = Flask(__name__)
@app.route('/partner/webhook/success', methods=['POST'])
def handle_upi_intent_webhook():
webhook_data = request.json
# Verify hash
if not verify_upi_intent_webhook_hash(webhook_data, "your_client_secret"):
return jsonify({"error": "Invalid hash"}), 400
# Extract details
txnid = webhook_data.get('txnid')
mihpayid = webhook_data.get('mihpayid')
status = webhook_data.get('status')
mode = webhook_data.get('mode')
bankcode = webhook_data.get('bankcode')
amount = webhook_data.get('amount')
# Update database
# db.update_payment_status(txnid=txnid, mihpayid=mihpayid, status=status)
print(f"✅ UPI Intent Payment: {status} | {txnid} | PayU ID: {mihpayid} | Mode: {mode}/{bankcode} | Amount: ₹{amount}")
# Respond with 200 OK
return jsonify({"message": "Webhook received"}), 200
if __name__ == '__main__':
app.run(port=5000)Step 4: Verify Payment
Step 4.1: Generate Verify Payment Hash
Hash Formula:
merchant_id|verify_payment|txnid|client_secret
Python:
import hashlib
def generate_verify_hash(merchant_id, txnid, client_secret):
hash_string = f"{merchant_id}|verify_payment|{txnid}|{client_secret}"
return hashlib.sha512(hash_string.encode('utf-8')).hexdigest()
verify_hash = generate_verify_hash(8739528, "UPIINT20240315001", "your_client_secret")Step 4.2: Call Verify Payment API
Endpoint:
| Environment | URL |
|---|---|
| Test | https://test-partnerapilayer.payu.in/apilayer/partner/verifyPayment |
| Production | https://api.payu.in/partner/verifyPayment |
Request:
curl --location 'https://test-partnerapilayer.payu.in/apilayer/partner/verifyPayment' \
--header 'Authorization: Bearer your_access_token_here' \
--header 'Content-Type: application/json' \
--data '{
"txnid": "UPIINT20240315001",
"merchant_id": 8739528,
"reseller_id": "11ee-0e7e-5403fde2-9523-0a696b110fde",
"hash": "computed_verify_hash_here"
}'Python:
import requests
import json
url = "https://test-partnerapilayer.payu.in/apilayer/partner/verifyPayment"
headers = {
'Authorization': 'Bearer your_access_token_here',
'Content-Type': 'application/json'
}
payload = {
"txnid": "UPIINT20240315001",
"merchant_id": 8739528,
"reseller_id": "11ee-0e7e-5403fde2-9523-0a696b110fde",
"hash": "computed_verify_hash_here"
}
try:
response = requests.post(url, headers=headers, data=json.dumps(payload))
print(f"Status: {response.status_code}")
print(f"Response: {response.text}")
except Exception as e:
print(f"Error: {str(e)}")Response:
{
"status": "success",
"unmappedstatus": "captured",
"mihpayid": "30478359672",
"txnid": "UPIINT20240315001",
"amount": "500.00",
"mode": "UPI",
"bankcode": "INTENT",
"productinfo": "UPI Payment for Order #12345",
"firstname": "Rajesh",
"email": "[email protected]",
"phone": "919876543210"
}Step 4.3: Process Verification Response
Reconciliation:
Compare webhook vs. verify response:
✅ mihpayid matches
✅ txnid matches
✅ amount matches
✅ status is "success"
✅ unmappedstatus is "captured"
✅ mode is "UPI"
✅ bankcode is "INTENT"
If all match, mark the transaction as confirmed.
Use Cases
Partner Payment UPI Intent is ideal for:
- Mobile-first apps — Ride-hailing, food delivery, e-commerce apps
- Quick checkout — Minimize steps and friction
- In-app purchases — Games, content subscriptions, digital goods
- Instant payments — Bills, recharges, peer-to-peer transfers
Error Handling
| Error | Cause | Resolution |
|---|---|---|
s2s_client_ip is mandatory | Missing customer IP when txn_s2s_flow=4 | Capture real customer IP from request headers (check X-Forwarded-For if behind proxy). Never use server IP |
s2s_device_info is mandatory | Missing device user-agent when txn_s2s_flow=4 | Capture device user-agent from HTTP request headers (User-Agent). Never hardcode or leave blank |
Invalid hash | Hash computation mismatch | Verify using client_secret, check 6-pipe sequence, ensure SHA-512 lowercase hex |
Invalid access token | OAuth token expired | Refresh OAuth token. Implement auto-refresh logic before expiry |
Transaction not found | txnid doesn't exist | Verify txnid in verify request matches creation request |
HMAC validation failure | Webhook hash verification failed | Check reverse hash formula (5 pipes after status). Use case-insensitive comparison |
| UPI app not opening | No UPI app installed or deep link issue | Check if UPI app is installed before invoking. Provide fallback message |
Testing
Test Environment
API Base URL: https://test-partnerapilayer.payu.in/apilayer/partner
OAuth URLs:
- Auth Code: https://uat-partner.payu.in/api/v1/merchants/auth_code
- Access Token: https://uat-accounts.payu.in/oauth/token
Test Workflow
1. Generate OAuth access token
2. Initiate UPI Intent payment with txn_s2s_flow=4
3. Verify intentURIData is returned in response
4. Test UPI app invocation on Android/iOS test devices
5. Complete test payment in UPI app
6. Verify webhook is received
7. Call Verify Payment API
8. Reconcile webhook vs verification response
Validation Checklist
✅ OAuth token generation succeeds
✅ Payment API returns intentURIData
✅ UPI app opens with pre-filled details
✅ Test payment succeeds in UPI app
✅ Webhook received within 5-10 seconds
✅ Webhook hash verification passes
✅ Verify Payment API confirms status
✅ Reconciliation successful
Best Practices
Capturing S2S Parameters
- ✅ Always capture real customer IP — Check X-Forwarded-For, X-Real-IP headers if behind proxy/CDN
- ✅ Capture accurate user-agent — Use the actual HTTP User-Agent header, never hardcode
- ✅ Never use server IP as s2s_client_ip — This will cause validation failures
UPI App Invocation
- ✅ Check if UPI app is installed before attempting to open deep link
- ✅ Provide fallback UI if no UPI app is installed ("Please install Google Pay or PhonePe")
- ✅ Show waiting screen after invoking UPI app with "Completing payment..." message
- ✅ Handle app-switch timeout — Update UI if customer doesn't return within 2-3 minutes
Security
- ✅ Always verify webhook hash before updating payment status
- ✅ Secure client_secret storage — Never expose in client-side code
- ✅ Use HTTPS for all webhook endpoints
- ✅ Implement rate limiting on webhook handlers
Reliability
- ✅ Implement idempotency using txnid to prevent duplicate processing
- ✅ Use unique txnid per transaction — Never reuse
- ✅ Handle "pending" status gracefully — Don't show "failed" immediately
- ✅ Implement webhook retry logic — PayU retries webhooks, handle duplicates
- ✅ Always call Verify Payment API after webhook for final confirmation
Integration
- ✅ Implement OAuth token refresh — Tokens expire after ~1 hour
- ✅ Log all API requests/responses for debugging
- ✅ Monitor webhook latency — Alert if webhooks delayed beyond expected time
- ✅ Test on real devices — Emulators may not handle UPI deep links correctly
Next Steps
- Payment Links Hosted Checkout — Multi-payment method web-based checkout
- Partner Payment UPI TPV Integration — UPI Intent with third-party verification
- Verify Payment API Reference — Complete API documentation
- Partner Webhook Configuration Guide — Advanced webhook handling
Updated about 1 hour ago
