Fonepay
Needs keysFonepay web redirect. A signed GET redirect; Fonepay signs its return and verifies with the UID it returns.
Environment
What this test shop reads. Values are never shown. Your own app can pass the same values to
Config however it likes.
FONEPAY_MERCHANT_CODErequired
Merchant code (PID)
Missing
FONEPAY_SECRET_KEYrequired
HMAC-SHA512 secret
Missing
FONEPAY_MERCHANT_CODE= FONEPAY_SECRET_KEY=
Integrate in your Go app
The same five steps work for every provider — only the constructor changes.
-
Install
go get github.com/mukezhz/pay-np
-
Create the provider
Build it once at startup and keep it as a
paynp.Provider. Load secrets from your config, never the browser.import ( paynp "github.com/mukezhz/pay-np" "github.com/mukezhz/pay-np/fonepay" ) p, err := fonepay.New(fonepay.Config{ MerchantCode: "NBQM", SecretKey: secret, }) if err != nil { log.Fatal(err) } var provider paynp.Provider = p -
Start a payment
Generate a unique TxnID per attempt and store it with the amount.
co.Writeredirects or renders the auto-submit form.func pay(w http.ResponseWriter, r *http.Request) { order := loadOrder(r) // your amount, never the browser's txnID := newTxnID() // unique per attempt, e.g. "ord-42-a1" ret := "https://shop.example/return/" + txnID co, err := provider.Initiate(r.Context(), paynp.InitiateRequest{ TxnID: txnID, Amount: order.Amount, // paynp.Paisa: Rs 1,500 = 150000 Description: order.Title, SuccessURL: ret, FailureURL: ret + "?failed=1", }) if err != nil { http.Error(w, "payment unavailable", http.StatusBadGateway) return } saveAttempt(txnID, order.ID, order.Amount, co.ProviderRef) co.Write(w, r) } -
Handle the return, then Lookup
The redirect is only a hint. Fulfil only when Lookup says
SUCCESS; it already checks the amount.func paymentReturn(w http.ResponseWriter, r *http.Request) { _ = r.ParseForm() cb, _ := provider.ParseCallback(r.Form) // may fail on cancel; that's fine a := loadAttempt(r.PathValue("txn")) // amount comes from YOUR records tx, err := provider.Lookup(r.Context(), paynp.LookupRequest{ TxnID: a.TxnID, Amount: a.Amount, ProviderRef: a.Ref, Callback: cb, }) switch { case errors.Is(err, paynp.ErrAmountMismatch): flagForReview(a) // never fulfil case err != nil: // provider unreachable: leave pending, the reconciler retries case tx.Status == paynp.StatusSuccess: fulfilOnce(a.OrderID, tx.ProviderRef) // idempotent case tx.Status.Final(): markFailed(a, tx.Status) } http.Redirect(w, r, "/orders/"+a.OrderID, http.StatusSeeOther) } -
Reconcile pending payments
Users close the tab after paying and Fonepay sends no webhook. Re-run Lookup on attempts that are not final. Fonepay's Lookup needs the callback UID, so attempts without a callback cannot be reconciled.
for range time.Tick(time.Minute) { for _, a := range pendingAttempts(olderThan(2 * time.Minute)) { tx, err := provider.Lookup(ctx, paynp.LookupRequest{ TxnID: a.TxnID, Amount: a.Amount, ProviderRef: a.Ref, }) if err == nil && tx.Status == paynp.StatusSuccess { fulfilOnce(a.OrderID, tx.ProviderRef) } else if err == nil && tx.Status.Final() { markFailed(a, tx.Status) } } }
Mobile apps (Android, iOS, Flutter)
Apps go through your backend: create the payment, open the checkout in an in-app browser, and let the provider return to your server, which runs Lookup and redirects to the app.
In-app browser only. The user must come back through the return URL: Fonepay’s Lookup needs its UID.
POST http://localhost:8080/api/payments
{"provider": "fonepay", "amount": "100.00", "app_return_url": "paynp://payment-done"}
→ 201 {"txn_id": "np-…", "status": "PENDING", "provider_ref": "…",
"checkout_url": "http://localhost:8080/pay/np-…"}
1. Open checkout_url in Custom Tabs / ASWebAuthenticationSession.
2. The provider returns to http://localhost:8080/return/fonepay/np-…; the server runs Lookup
and redirects to paynp://payment-done?txn_id=np-…&status=SUCCESS.
3. Confirm before fulfilling: GET http://localhost:8080/api/payments/np-…
→ {"status": "SUCCESS", "final": true}Notes
- PRN (TxnID) must be 3–25 characters.
- Lookup needs the callback's UID, so abandoned payments cannot be reconciled with this flow.