Hamro Pay

Needs keys

Hamro 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.

Form POST (session) Unsigned callback Get Transaction + signed webhook import "github.com/mukezhz/pay-np/hamropay"

Environment

What this test shop reads. Values are never shown. Your own app can pass the same values to Config however it likes.

VariablePurposeStatus
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
.env
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.

  1. Install

    terminal
    go get github.com/mukezhz/pay-np
  2. Create the provider

    Build it once at startup and keep it as a paynp.Provider. Load secrets from your config, never the browser.

    setup.go
    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
  3. Start a payment

    Generate a unique TxnID per attempt and store it with the amount. co.Write redirects or renders the auto-submit form.

    pay.go
    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)
    }
  4. 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.

    return.go
    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)
    }
  5. 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.

    webhook.go
    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)
    }
  6. Reconcile pending payments

    Users close the tab after paying. Re-run Lookup on attempts that are not final.

    reconcile.go
    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.

app ↔ backend
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).