Fonepay reference
Source:
references/fonepay.md. This is the same file the agent reads.
Sources: official “Technical Specification Document (Fonepay Web Integration) v2.0” (Sept 2020), the Dynamic QR spec v1.1 (2024, community transcription matched by two independent codebases), official Java samples, and live dev-gateway tests (2026-11).
Adapters: scripts/ts/src/fonepay.ts, scripts/python/nepal_payments/fonepay.py.
1. Environments and credentials
Section titled “1. Environments and credentials”| Purpose | Dev | Live |
|---|---|---|
| Web payment redirect | https://dev-clientapi.fonepay.com/api/merchantRequest |
https://clientapi.fonepay.com/api/merchantRequest |
| txnVerification (POST) | https://dev-merchantapi.fonepay.com/api/merchant/merchantDetailsForThirdParty/txnVerification* |
https://merchantapi.fonepay.com/api/merchant/merchantDetailsForThirdParty/txnVerification |
| Legacy verification (GET, XML) | https://dev-clientapi.fonepay.com/api/merchantRequest/verificationMerchant |
https://clientapi.fonepay.com/api/merchantRequest/verificationMerchant |
| Dynamic QR | https://dev-merchantapi.fonepay.com/api/merchant/merchantDetailsForThirdParty/…* |
https://merchantapi.fonepay.com/api/merchant/merchantDetailsForThirdParty/… |
| WebSocket | wss://dev-ws.fonepay.com/convergent-webSocket-web/merchantEndPoint* |
wss://ws.fonepay.com/convergent-webSocket-web/merchantEndPoint |
| Merchant panel | https://dev-merchant-login.fonepay.com | https://login.fonepay.com |
* Dev merchant-API hosts are not in public DNS; Fonepay issues or whitelists them during onboarding. Override them with the adapter’s txnVerificationUrl / qrBaseUrl options.
Credentials (from your acquiring bank / Fonepay onboarding):
PID: merchant code, 3–20 chars.- Secret key: the HMAC key, shown on the merchant profile page. Test and live keys differ.
- Merchant-panel username + password: needed for txnVerification (recommended) and Dynamic QR.
Public dev sample from the official docs: PID=NBQM, secret a7e3512f5032480a83137793cb2021dc. The dev gateway accepted it in 2026-11 tests, so you can test redirects without onboarding.
Env vars: FONEPAY_ENV, FONEPAY_MERCHANT_CODE, FONEPAY_SECRET_KEY, FONEPAY_USERNAME, FONEPAY_PASSWORD.
2. Web payment flow
Section titled “2. Web payment flow”- Server builds params + DV, then redirects the browser (GET, URL-encoded query) to
merchantRequest. - Fonepay answers
303to its payment page. The payer pays with a bank app or scans the QR. - Fonepay redirects to
RUwithPRN, PID, PS, RC, UID, BC, INI, P_AMT, R_AMT, DV. - Server verifies the response DV, then calls txnVerification (or the legacy verification).
| Param | Rule |
|---|---|
PID |
merchant code |
MD |
P |
PRN |
your attempt id, 3–25 chars, unique (Duplicate PRN → HTTP 409) |
AMT |
rupees (10, 10.50) |
CRN |
NPR |
DT |
MM/DD/YYYY in Asia/Kathmandu |
R1 |
description, ≤ 160 chars |
R2 |
remarks, ≤ 50 chars, N/A if none |
RU |
return URL, ≤ 150 chars |
DV |
HMAC (below) |
Callback params are trusted only with a valid DV. A missing DV counts as invalid: otherwise anyone could send ?PRN=<id>&PS=false and force a failure. The adapters throw invalid_signature (strict mode) and never derive a status from unsigned flags.
Return params: PS = true/false. RC = successful / failed / cancel. UID = Fonepay trace id (store it). BC = paying bank SWIFT or N/A. INI = initiator. P_AMT = amount paid (can differ due to charges or discounts). R_AMT = amount requested by the merchant (not a refund amount, despite some community docs).
3. Signatures (DV) + test vectors
Section titled “3. Signatures (DV) + test vectors”HMAC-SHA512(key = secret UTF-8, message = values joined by ",") → hex. The official Java sample emits uppercase and PHP hash_hmac emits lowercase. The dev gateway accepted both in live tests, so compare case-insensitively.
| Use | Field order |
|---|---|
| Request DV | PID,MD,PRN,AMT,CRN,DT,R1,R2,RU (raw values, not URL-encoded) |
| Response DV | PRN,PID,PS,RC,UID,BC,INI,P_AMT,R_AMT (raw strings as received, e.g. 20.0) |
txnVerification auth header |
username,password,POST,application/json,/merchant/merchantDetailsForThirdParty/txnVerification,<exact JSON body> |
QR create dataValidation |
amount,prn,merchantCode,remarks1,remarks2 (lowercase hex in samples) |
QR status dataValidation |
prn,merchantCode |
| Legacy verification DV | PID,AMT,PRN,BID,UID |
Verified vectors (secret a7e3512f5032480a83137793cb2021dc):
- Response DV of
d1580724437729,NBQM,true,successful,36463,NICENPKA,9841845631,20.0,30→667860224021DF1891F7DE873A37B1DEDA720CCDC43F63BC88F86ED20F579E0DE66526D37C71B1D14A8D466E4B740D17D4FF274CD2819FD6ED2AA3D9A89D7C52 authforgnw71090@zwoho.com,Fonepay@12345,POST,application/json,/merchant/merchantDetailsForThirdParty/txnVerification,{"prn": "d1549791827186","merchantCode": "NBQM","amount": "30"}→0d3ebe65246182596de0d417e8e39d37ebe2f5da028c52ae0a33a05062a3bded8a737c3ff82b6f1a6d9442d11cfcb0432d86998af81f481948c0af7b346f4fa2- QR
14,5d76d323-d1f6,NBQM,test1,test2→5ae718032328ae5615fa4694dfbf3ecd13432bd49031e7285a3f8bc122ecbb8e833e83c7a11f895d30f3e8fd1906317aece113675166ae0d804ecf32a8bbdbaf
The spec’s request DV example (099d85…) doesn’t reproduce with any field variant. Use the live gateway instead: a bad DV returns 409 {"message":"Data Validation Failed"}, and a good one returns 303 to the payment page.
strtoupper(hash_hmac('sha512', implode(',', [$pid,'P',$prn,$amt,'NPR',$dt,$r1,$r2,$ru]), $secret));hmac.new(secret.encode(), ",".join(values).encode(), hashlib.sha512).hexdigest().upper()m := hmac.New(sha512.New, []byte(secret)); m.Write([]byte(strings.Join(values, ","))); strings.ToUpper(hex.EncodeToString(m.Sum(nil)))4. Verification
Section titled “4. Verification”txnVerification (recommended; works without a callback, so reconciliation can use it)
Section titled “txnVerification (recommended; works without a callback, so reconciliation can use it)”POST …/txnVerificationAuthorization: Basic base64(username:password)auth: <hmac hex>Content-Type: application/json
{"prn": "d1549791827186","merchantCode": "NBQM","amount": "30"}Response: {fonepayTraceId, merchantCode, paymentStatus, prn, requestedAmount, totalTransactionAmount, transactionDate, initiator}. paymentStatus is success → paid, pending → pending, failed → failed. Errors: 401 {"errorCode":401,"message":"cannot access this resource"}, or a validation array.
The auth HMAC covers the exact body bytes. Build the JSON string once and send that same string. Don’t let your HTTP client re-serialize it (Python’s json.dumps adds spaces after commas, a known community bug). The adapters reproduce the spec’s format (": " after keys, no space after commas) and match the spec vector byte for byte.
Legacy GET verification (2019, still answers)
Section titled “Legacy GET verification (2019, still answers)”GET …/verificationMerchant?PRN&PID&BID(=BC)&AMT&UID&DV returns XML. Success is <response_code>successful</response_code> and <success>true</success>. A non-200 or unparseable reply throws (the attempt stays open); it never reads as “not paid”. It needs UID and BC from the redirect, so it can’t reconcile abandoned sessions. The adapters use it only when no username/password is configured.
5. Dynamic QR + WebSocket
Section titled “5. Dynamic QR + WebSocket”For POS, kiosks, desktop checkouts and “scan to pay” screens. Needs username/password.
qrCreate({orderId, amountPaisa, remarks1, remarks2})→ POSTthirdPartyDynamicQrDownloadwith{amount, remarks1, remarks2, prn, merchantCode, dataValidation, username, password}→{qrMessage, status:"CREATED", statusCode:201, thirdpartyQrWebSocketUrl}. RenderqrMessage(an EMV/NepalQR string) with any QR library. Errors return406 "Data Validation Failed".- Connect to
thirdpartyQrWebSocketUrl. Messages carrytransactionStatusas a JSON string: first{"QRVerified":true}(scanned), then{"paymentSuccess":true, traceId, amount}. The URL has been seen with alocalhosthost; the adapter rewrites it tows.fonepay.com. - Always confirm with
qrStatus(orderId)→ POSTthirdPartyDynamicQrGetStatus(paymentStatus:success/failed/pending) before fulfilling. Never trust the socket alone.
const { qrMessage, webSocketUrl } = await fp.qrCreate({ orderId, amountPaisa, remarks1: "Table 4" });const ws = new WebSocket(webSocketUrl!);ws.onmessage = async (ev) => { const msg = JSON.parse(ev.data); const st = typeof msg.transactionStatus === "string" ? JSON.parse(msg.transactionStatus) : msg.transactionStatus; if (st?.paymentSuccess) { const v = await fp.qrStatus(orderId); // authoritative if (v.status === "paid") await markPaid(orderId); // same conditional update as architecture.md §4 ws.close(); }};// Also poll qrStatus every ~5 s as a fallback in case the socket drops.Amount units: the official examples use rupees ("14"). One community SDK sends paisa; the adapters follow the official examples.
6. Gotchas
Section titled “6. Gotchas”- AMT in DV must be byte-identical to AMT sent.
10≠10.00≠10.0. The adapters format once withpaisaToRupees. DTuses Nepal time. Near midnight UTC, Nepal is already on the next day.fonepayDate()handles that.- PRN length: max 25 on the web flow (UUIDs don’t fit; use
newOrderId()), max 50 on txnVerification. - Response DV uses raw received strings. Don’t parse or reformat
P_AMTbefore hashing. - Never compute DV on a client. Some unofficial Flutter packages do; that ships your secret.
- Hosts:
dev.fonepay.com/api/merchantRequestandfonepay.com/api/merchantRequest(seen in some starters) return 404. Use theclientapihosts above. - Refunds: no public refund API. Use the merchant panel or your bank.
thirdPartyPostTaxRefundis a tax refund, not a payment refund. - “Checkout by Fonepay” (launched July 2026) and a newer “third-party v2” API (RSA-signed, Intent QR) exist without public specs. Ask Fonepay if you’re starting fresh.