How Fonepay Works

โ„น๏ธ Fonepay uses a URL redirect model โ€” similar to eSewa but instead of a form POST, the user is redirected directly via URL. Your backend builds a signed redirect URL. The frontend navigates to it.
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

ModeGateway 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
    }
}
โš ๏ธ Amount is NPR as a double โ€” not paisa like Khalti. NPR 100 โ†’ send 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:

ParameterDescriptionExample
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

๐Ÿ”ด Never trust the PS parameter alone. Anyone can type ?PS=success in the URL. Always call verifyCallback() which checks the HMAC signature.
๐Ÿ”ด Never hardcode your Fonepay secret key. Use environment variables.
โœ… Store the PRN in your database before returning the redirect URL.
โœ… verifyCallback() throws FonepayException if the signature does not match โ€” treat this as a potential fraud attempt.