Hamro Pay
Needs keysHamro Pay Checkout. The server creates a session, signs a token, and the browser form-POSTs to the gateway. Hamro Pay also sends a signed webhook.
Environment
What this test shop reads. Values are never shown. Your own app can pass the same values to
Config however it likes.
HAMROPAY_MERCHANT_IDrequired
Merchant ID
Missing
HAMROPAY_CLIENT_IDrequired
Client-Id header
Missing
HAMROPAY_CLIENT_API_KEYrequired
Client-API-Key header
Missing
HAMROPAY_CLIENT_SECRETrequired
HMAC-SHA512 signing secret
Missing
HAMROPAY_WEBHOOK_SECRET
Webhook signing secret (Configuration → Webhook)
Optional
HAMROPAY_API_BASE_URL
Production API URL (issued at live onboarding)
Optional
HAMROPAY_GATEWAY_URL
Production gateway URL (issued at live onboarding)
Optional
HAMROPAY_MERCHANT_ID= HAMROPAY_CLIENT_ID= HAMROPAY_CLIENT_API_KEY= HAMROPAY_CLIENT_SECRET=
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/hamropay" ) p, err := hamropay.New(hamropay.Config{ MerchantID: mid, ClientID: cid, ClientAPIKey: apiKey, ClientSecret: secret, WebhookSecret: hookSecret, // for ParseWebhook }) 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 and is unsigned. 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) } -
Receive the signed webhook
Register your endpoint in the UAT portal (Configuration → Webhook) and set
WebhookSecret. Hamro Pay's webhook is its only signed status source.func hamropayWebhook(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(io.LimitReader(r.Body, 1<<20)) cb, err := hp.ParseWebhook(r.Header, body) // hp is the *hamropay.Client if err != nil { http.Error(w, "invalid", http.StatusUnauthorized) return } a := loadAttempt(cb.TxnID) if cb.Status == paynp.StatusSuccess && cb.Amount == a.Amount { fulfilOnce(a.OrderID, cb.TxnID) } w.WriteHeader(http.StatusOK) } -
Reconcile pending payments
Users close the tab after paying. Re-run Lookup on attempts that are not final.
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 via checkout_url. Hamro Pay’s signed webhook also confirms payments the app never returns from.
POST http://localhost:8080/api/payments
{"provider": "hamropay", "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/hamropay/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
- Amount must be Rs 10–50,000; TxnID ≤ 25 characters without commas.
- Test wallet 9841414141, T-PIN 0000. Checkout OTP 000000 succeeds, 111111 stays pending, 222222 fails.
- Webhooks arrive at {BASE_URL}/webhook/hamropay and need a public URL (e.g. a tunnel).