Skip to content

Getting Started

Amir Iranmanesh edited this page Jul 31, 2026 · 1 revision

Getting Started

Install

go get github.com/amiranmanesh/payvand

Go 1.26 or newer. There is nothing else to install: Payvand has no dependencies.

Initialise once

payvand.Init holds the settings shared by your whole application. Build it at start-up and keep it.

pv := payvand.Init(
    payvand.WithTimeout(20*time.Second),
    payvand.WithUserAgent("shop/1.4"),
    payvand.WithLogger(payvand.SlogLogger{Logger: slog.Default()}),
)

Build a gateway

gw, err := pv.Gateway(payvand.Zarinpal, payvand.Config{
    MerchantKey: os.Getenv("ZARINPAL_MERCHANT_ID"),
})
if err != nil {
    log.Fatal(err) // credentials are validated here, not at payment time
}

The first argument is data. It can come from a configuration file, an environment variable or the merchant's row in your database — that is the whole point of the registry. See Configuration for what each provider needs.

Gateways are safe for concurrent use; build one per terminal and share it.

Create a payment

purchase, err := gw.Purchase(ctx, payvand.PurchaseRequest{
    Amount:      payvand.Toman(15_000),
    OrderID:     order.ID,                   // your unique id; several banks require it to be numeric
    CallbackURL: "https://shop.example/payments/callback",
    Description: "Wallet top-up",
    Mobile:      "09120000000",              // lets the bank page offer saved cards
})
if err != nil {
    return err
}

order.PaymentToken = purchase.Token
order.Save()                                  // persist BEFORE redirecting

Send the payer to the bank

purchase.Redirect.Send(w, r)

That single line covers both worlds: a 303 redirect for GET gateways, and an auto-submitting HTML form for the ones that require a POST (Mellat, Sepehr, Iran Kish, AsanPardakht). If you render the page yourself:

purchase.Redirect.String()   // full URL with parameters appended
purchase.Redirect.IsPost()   // whether a form is required
purchase.Redirect.HTML()     // the auto-submitting form as a string

Finish the payment

func callbackHandler(w http.ResponseWriter, r *http.Request) {
    cb, err := gw.ParseCallback(r)
    if err != nil {
        http.Error(w, "unreadable callback", http.StatusBadRequest)
        return
    }
    if !cb.Succeeded {
        orders.MarkCanceled(cb.Token)
        http.Error(w, "the payer canceled the payment", http.StatusPaymentRequired)
        return
    }

    order := orders.ByToken(cb.Token)

    verified, err := gw.Verify(r.Context(), cb.VerifyRequest(order.Amount))
    switch {
    case errors.Is(err, payvand.ErrAlreadyVerified):
        // a refreshed callback page: the order is already paid
    case err != nil:
        orders.MarkFailed(order.ID, err)
        http.Error(w, "the payment could not be verified", http.StatusPaymentRequired)
        return
    }

    orders.MarkPaid(order.ID, verified.ReferenceNumber, verified.CardNumber)
}

Verify is where the multi-step providers are hidden: Mellat verifies and settles, AsanPardakht reads the transaction then verifies then settles, Vandar reads the transaction then verifies, Pasargad checks then verifies. You call one method.

Refund and inquiry

if gw.Capabilities().Refund {
    _, err := gw.Refund(ctx, payvand.RefundRequest{
        Token:           order.PaymentToken,
        OrderID:         order.ID,
        ReferenceNumber: order.ReferenceNumber,
        Amount:          order.Amount,
    })
}

if gw.Capabilities().Inquiry {
    state, err := gw.Inquiry(ctx, payvand.InquiryRequest{
        Token:   order.PaymentToken,
        OrderID: order.ID,
    })
    // state.Status: pending, paid, verified, failed, canceled, refunded
}

Mistakes worth avoiding

Mistake Why it hurts
Trusting cb.Succeeded and skipping Verify the bank reverses the transaction and the payer gets the money back
Taking the amount from the callback a payer can pay 1,000 Rial for a 1,000,000 Rial order
Redirecting before persisting the token you cannot match the callback to an order
Ignoring ErrAlreadyVerified a refreshed callback page turns a paid order into a failed one
Assuming a unit some providers take Rial, PayPing takes Toman; use payvand.Rial / payvand.Toman and let the gateway convert
A non-numeric OrderID Sadad, TOP, Mellat and Sepehr reject it

Run the examples

git clone https://github.com/amiranmanesh/payvand
cd payvand
go run ./examples/basic         # a full cycle, offline
go run ./examples/multigateway  # the capability table, and switching providers
go run ./examples/webshop       # a two-handler shop on :8080

Next: Configuration · Callbacks and Verification

Clone this wiki locally