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

Errors

Every failure is a *payvand.Error wrapping a sentinel, so you can match broadly and still read the provider's own code.

type Error struct {
	Gateway Name   // "mellat"
	Op      string // "purchase", "verify", "refund", "inquiry", "callback", "new"
	Code    string // the provider's status code, when it published one
	Message string // the provider's message, in English where Payvand knows it
	Err     error  // the sentinel it unwraps to
}

Sentinels

Sentinel Meaning Usual reaction
ErrNotSupported the provider has no such API check Capabilities() first
ErrGatewayNotRegistered unknown gateway name fix the configuration; fail at start-up
ErrInvalidConfig missing or malformed credentials fix the terminal; this only fires in New
ErrInvalidRequest the request cannot be sent fix the caller: zero amount, missing order id, non-numeric id
ErrPaymentFailed the provider rejected the operation read Code and Message; usually final
ErrPaymentCanceled the payer aborted mark the order canceled
ErrAlreadyVerified the transaction was verified before treat as success, do not double-ship
ErrAmountMismatch the settled amount differs stop and reconcile by hand
ErrUnexpectedResponse the body could not be understood provider outage or a protocol change; retry, then alert

Matching

verified, err := gw.Verify(ctx, req)
switch {
case errors.Is(err, payvand.ErrAlreadyVerified):
    // fine

case errors.Is(err, payvand.ErrAmountMismatch):
    alert("amount mismatch on order " + order.ID)

case errors.Is(err, payvand.ErrPaymentFailed):
    var e *payvand.Error
    errors.As(err, &e)
    log.Printf("gateway=%s op=%s code=%s message=%s", e.Gateway, e.Op, e.Code, e.Message)

case err != nil:
    // transport, timeout, unexpected body
}

Provider code tables

Gateways whose provider publishes a code table expose it, translated into English:

mellat.Message("421")     // "the ip address is invalid"
irankish.Message("51")    // "insufficient funds"
saman.Message(-5)         // "the transaction was already verified"
nextpay.Message(-49)      // "the transaction was already verified"
bitpay.Message(-4)        // "the api key is invalid"

The raw code is always in Error.Code, so you can log it and look it up in the provider's own documentation.

What the payer should see

Provider messages are written for merchants, not for payers, and some leak internal detail. A safe mapping:

Situation Message to the payer
ErrPaymentCanceled, Succeeded == false "You canceled the payment."
ErrPaymentFailed with a card-side code (insufficient funds, wrong PIN, expired card) "Your bank declined the payment. Please try another card."
ErrAmountMismatch, ErrUnexpectedResponse, transport errors "We could not confirm your payment. If money left your account it will be returned within 72 hours."
Anything else a generic failure, plus the order id for support

Keep Error.Code, Error.Message and the Raw body in your own logs — that is what the PSP's support desk will ask for.

Timeouts and retries

WithRetry(n, backoff) retries network errors and 5xx responses only. It never retries a 4xx, because those are business rejections.

A timeout on Purchase is harmless: no token, no payment. A timeout on Verify is not — the provider may have settled the transaction anyway. Treat it as unknown, and resolve it with Inquiry rather than by assuming failure.

Next: Testing · Callbacks and Verification

Clone this wiki locally