How Fonepay Works
Your Backend
โ buildRedirectParams() โ generates HMAC-SHA512 signature
โ returns redirectUrl to frontend
โ
Frontend: window.location.href = redirectUrl
โ
User pays on Fonepay (QR, bank, wallet)
โ
Fonepay redirects to your return URL with query params:
?PRN=xxx&PID=xxx&PS=success&RC=200&UID=xxx&BC=xxx&INI=xxx&P_AMT=100&R_AMT=0&DV=HMAC_HEX
โ
โ ๏ธ NEVER trust redirect params alone!
โ
Your Backend: verifyCallback() โ re-computes HMAC-SHA512 and verifies DV
โ
โ
mark as paid when PS = "success" AND signature matches
Key Differences from Other Gateways
| Feature | Khalti | eSewa | Fonepay |
|---|---|---|---|
| Flow type | API-first POST | Form POST | URL redirect (GET) |
| Signature | None (API key) | HMAC-SHA256 Base64 | HMAC-SHA512 hex |
| Amount unit | Paisa (NPR ร 100) | NPR (BigDecimal) | NPR (double) |
| Callback format | Query params | Base64 JSON | Query params |
| Verify method | Lookup API call | HMAC + status API | HMAC only (no API) |
| RestClient needed | โ Yes | โ Yes | โ No |
Configure
nepalpay:
fonepay:
merchant-code: ${FONEPAY_MERCHANT_CODE} # your PID from Fonepay
secret-key: ${FONEPAY_SECRET_KEY} # HMAC secret from Fonepay
return-url: ${FONEPAY_RETURN_URL} # your callback URL
sandbox: true # false for production
FonepayClient is auto-configured when
nepalpay.fonepay.secret-key is present.
No additional setup required.
Sandbox vs Production
| Mode | Gateway URL |
|---|---|
Sandbox (sandbox: true) |
https://dev.fonepay.com/api/merchantRequest |
Production (sandbox: false) |
https://fonepay.com/api/merchantRequest |
buildRedirectParams()
Builds signed redirect parameters and constructs the full redirect URL. The HMAC-SHA512 signature is computed automatically from the correct field order.
Simple overload โ just amount and PRN
@Service
@RequiredArgsConstructor
public class PaymentService {
private final FonepayClient fonepayClient;
public String startFonepayPayment(String orderId, double amountNPR) {
// PRN = Product Reference Number (3-25 chars, must be unique)
String prn = ("FP-" + orderId).substring(0, Math.min(25, orderId.length() + 3));
// IMPORTANT: save prn to your DB before returning redirect URL!
FonepayRedirectParams params = fonepayClient.buildRedirectParams(
FonepayPaymentRequest.builder()
.prn(prn)
.amount(amountNPR) // NPR directly as double
.remarks1("Pro Plan") // shown to user on Fonepay
.remarks2("NepalPay") // optional
.build()
);
return params.redirectUrl(); // redirect user to this URL
}
}
100.0.
PRN rules
- Must be between 3 and 25 characters
- Must be unique per transaction
- Alphanumeric and hyphens recommended
- Store in your database BEFORE returning the redirect URL
Signature message format (MANDATORY order)
// NepalPay builds this internally โ you don't need to do it manually
// Order is: PID,MD,PRN,AMT,CRN,DT,R1,R2,RU
// Hash: HMAC-SHA512(secretKey, message, UTF-8)
// Output: lowercase hexadecimal
Frontend Redirect
Your backend returns the signed redirect URL. The frontend navigates the user to it directly.
Angular / TypeScript
initiatePayment(orderId: string, amount: number): void {
this.paymentService.startFonepay(orderId, amount).subscribe(response => {
// Navigate user to Fonepay gateway
window.location.href = response.redirect_url;
});
}
Plain JavaScript
fetch('/api/payment/fonepay/initiate', {
method: 'POST',
body: JSON.stringify({ orderId: 'ORD-001', amountNPR: 100 })
})
.then(res => res.json())
.then(data => {
window.location.href = data.redirect_url;
});
verifyCallback()
Fonepay redirects to your return-url with all
payment details as GET query parameters.
Always verify the HMAC-SHA512 signature
before trusting the payment status.
@GetMapping("/api/payment/fonepay/callback")
public ResponseEntity<String> callback(
@RequestParam String PRN,
@RequestParam String PID,
@RequestParam String PS,
@RequestParam String RC,
@RequestParam String UID,
@RequestParam String BC,
@RequestParam String INI,
@RequestParam(name = "P_AMT") String pAmt,
@RequestParam(name = "R_AMT") String rAmt,
@RequestParam(name = "DV") String dv) {
// Build the typed callback response
FonepayCallbackResponse callback =
FonepayCallbackResponse.of(PRN, PID, PS, RC, UID, BC, INI, pAmt, rAmt, dv);
// verifyCallback() does TWO things automatically:
// 1. Re-computes HMAC-SHA512 and verifies DV signature (tamper protection)
// 2. Checks PS = "success"
FonepayClient.FonepayVerificationResult result =
fonepayClient.verifyCallback(callback);
if (!result.isPaymentSuccessful()) {
return ResponseEntity.badRequest().body("Payment not confirmed");
}
// Safe to mark order as paid
orderService.markAsPaid(PRN);
return ResponseEntity.ok("Payment successful");
}
Callback Parameters
Fonepay sends these as GET query parameters to your
return-url:
| Parameter | Description | Example |
|---|---|---|
PRN |
Your original Product Reference Number | FP-ORD-001 |
PID |
Fonepay merchant code | MERCHANT_CODE |
PS |
Payment status | success or failed |
RC |
Response code | 200 |
UID |
Fonepay unique transaction ID | fonepay-uid-001 |
BC |
Bank code of the paying bank | GBIME |
INI |
Transaction initiator | 9800000001 |
P_AMT |
Paid amount in NPR | 100 |
R_AMT |
Refund amount | 0 |
DV |
HMAC-SHA512 signature (uppercase hex) | A1B2C3... |
Response signature field order (MANDATORY)
// NepalPay verifies this automatically
// Order: PRN,PID,PS,RC,UID,BC,INI,P_AMT,R_AMT
// Hash: HMAC-SHA512(secretKey, message, UTF-8)
// Output: UPPERCASE hexadecimal
// Compare to: DV parameter from Fonepay (converted to uppercase)
Security Rules
?PS=success in the URL.
Always call verifyCallback() which checks the HMAC signature.
verifyCallback() throws FonepayException
if the signature does not match โ treat this as a potential fraud attempt.