-
Notifications
You must be signed in to change notification settings - Fork 0
Callbacks and Verification
This is the part that decides whether you actually get paid.
A payment is final when
Verifyreturns 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.
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 |
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)))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 |
Callbacks arrive twice more often than you would like: the payer refreshes, the bank retries, a proxy replays. Make the handler idempotent:
- Look the order up by
cb.Token. - If it is already paid, answer 200 and stop.
- Otherwise verify, then mark it paid in one transaction.
- Treat
ErrAlreadyVerifiedas success.
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.
| 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.
Payvand · MIT licensed · built on the Go standard library alone · report an issue