eSewa
ReadyeSewa ePay v2. The browser form-POSTs a signed request; eSewa returns a signed base64 payload and offers a status API.
Environment
What this test shop reads. Values are never shown. Your own app can pass the same values to
Config however it likes.
ESEWA_PRODUCT_CODErequired
Merchant product code · sandbox default: EPAYTEST
Built-in
ESEWA_SECRET_KEYrequired
HMAC-SHA256 signing secret · sandbox default: eSewa's published sandbox secret
Built-in
ESEWA_STATUS_URL
Override the status API URL
Optional
ESEWA_MOBILE_CLIENT_ID
SDK client ID (mobile verification) · sandbox default: eSewa's published SDK client
Built-in
ESEWA_MOBILE_CLIENT_SECRET
SDK client secret (mobile verification) · sandbox default: eSewa's published SDK secret
Built-in
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/esewa" ) p, err := esewa.New(esewa.Config{ ProductCode: esewa.SandboxProductCode, SecretKey: esewa.SandboxSecretKey, }) 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 eSewa sends no webhook. 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.
Open checkout_url in an in-app browser, or use eSewa’s native SDK and verify the refId on your server (below).
POST http://localhost:8080/api/payments
{"provider": "esewa", "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/esewa/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}eSewa native SDK
The SDK pays inside the app, so there is no Initiate. Verify the refId on your server before fulfilling.
// The app pays with eSewa's Android/iOS/Flutter SDK using a productId you
// generate per order, then sends your backend the refId the SDK returned.
app, err := esewa.NewMobile(esewa.MobileConfig{
ClientID: esewa.SandboxMobileClientID,
ClientSecret: esewa.SandboxMobileClientSecret,
})
tx, err := app.Verify(ctx, esewa.MobileVerifyRequest{
ProductID: order.ID, // binds the payment to this order
RefID: refIDFromApp, // optional; productId + amount also works
Amount: order.Amount, // from your records
})
if err == nil && tx.Status == paynp.StatusSuccess {
fulfilOnce(order.ID, tx.ProviderRef)
}Notes
- Whole rupees are sent as integers ("110"), matching eSewa's signing examples.
- The failure redirect carries no signed data, so the TxnID is kept in the return URL path.
- Status API maps COMPLETE → SUCCESS, PENDING/AMBIGUOUS → PENDING, NOT_FOUND → NOT_FOUND.
- Mobile SDK payments use different credentials (client ID/secret) and esewa.NewMobile; the SDK test login is 9711111111, Nepal@123, MPIN 1122, token 123456.