Skip to content

Callbacks and Verification

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

Callbacks and Verification

This is the part that decides whether you actually get paid.

The rule

A payment is final when Verify returns without an error. Not before.

Most Iranian gateways reverse a transaction that is never verified, usually within 15 to 30 minutes. Mellat and AsanPardakht additionally require a settlement call, which Payvand makes inside Verify.

Parsing the callback

Providers disagree about everything: some redirect with a query string, some post a form, and the field names drift in casing between them. ParseCallback normalises all of it.

cb, err := gw.ParseCallback(r)
Field Meaning
cb.Succeeded the bank's own verdict — a hint, not proof
cb.Token matches PurchaseResponse.Token; your lookup key
cb.OrderID your order id, when the bank echoes it
cb.ReferenceNumber bank reference (RRN), required by Iran Kish, Mellat, Saman
cb.TraceNumber system trace audit number, required by Iran Kish, Mellat
cb.CardNumber masked PAN, when present
cb.Code, cb.Message the raw provider status
cb.Values every parameter, so provider specific fields stay reachable
cb.Get("digitalreceipt") one of those fields, by name

Building the verification

order := orders.ByToken(cb.Token)
verified, err := gw.Verify(ctx, cb.VerifyRequest(order.Amount))

cb.VerifyRequest(amount) copies the token, order id, reference number, trace number, card number and the whole Values map — including the fields only one provider uses, such as Sepehr's digital receipt — and takes the amount from you, not from the request. Never do this:

// WRONG: the payer controls the browser, and therefore this number
amount, _ := strconv.ParseInt(r.URL.Query().Get("amount"), 10, 64)
gw.Verify(ctx, cb.VerifyRequest(payvand.Rial(amount)))

Handling the outcome

verified, err := gw.Verify(ctx, cb.VerifyRequest(order.Amount))
switch {
case err == nil:
    orders.MarkPaid(order.ID, verified.ReferenceNumber, verified.CardNumber)

case errors.Is(err, payvand.ErrAlreadyVerified):
    // The payer refreshed the callback page. The order is already paid;
    // treat it as success and do not double-ship.

case errors.Is(err, payvand.ErrAmountMismatch):
    // The bank settled a different amount. Stop, alert, reconcile by hand.
    orders.Flag(order.ID, err)

default:
    orders.MarkFailed(order.ID, err)
}

VerifyResponse carries what you keep for reconciliation:

Field Use
ReferenceNumber the RRN to show the payer and keep for support
TransactionID the provider side id, used by refunds
CardNumber, CardHash masked PAN and its hash, when the provider returns them
Amount what was actually settled
Fee provider fee, when reported
PaidAt settlement time, when reported
Raw the untouched provider body, for support tickets

Idempotency

Callbacks arrive twice more often than you would like: the payer refreshes, the bank retries, a proxy replays. Make the handler idempotent:

  1. Look the order up by cb.Token.
  2. If it is already paid, answer 200 and stop.
  3. Otherwise verify, then mark it paid in one transaction.
  4. Treat ErrAlreadyVerified as success.

Lost callbacks

Payers close the tab. When a payment stays pending, ask the provider:

if gw.Capabilities().Inquiry {
    state, err := gw.Inquiry(ctx, payvand.InquiryRequest{
        Token:   order.PaymentToken,
        OrderID: order.ID,
    })
    if err == nil && state.Status == payvand.StatusPaid {
        // the money was taken but never settled — verify now
        verified, err := gw.Verify(ctx, payvand.VerifyRequest{
            Token:   order.PaymentToken,
            OrderID: order.ID,
            Amount:  order.Amount,
        })
        _ = verified
        _ = err
    }
}

Run that as a job over pending orders older than a few minutes. Gateways without an inquiry API (Capabilities().Inquiry == false) have to be reconciled from the provider panel.

Providers that need extra callback fields

Provider Needed at verification Where it comes from
Iran Kish reference number and trace number the callback
Mellat sale reference id the callback (SaleReferenceId)
Saman RefNum the callback
Sepehr digital receipt the callback (digitalreceipt)
Pasargad invoice date + transaction reference PurchaseResponse.Extra + the callback (tref)
AsanPardakht local invoice id your order id
BitPay.ir trans_id and id_get the callback

Callback.VerifyRequest carries all of these automatically. The only one you must store yourself is the Pasargad invoice date, because it is produced at purchase time.

Next: Errors · Testing

Clone this wiki locally