Skip to content
Payments

A Practical Guide to pepperQik Error Codes and Resilient Payment Flows

Four families of payment errors, one correct reaction each: classification code, the recovery query that prevents double charges, dispute-winning logs and a test plan for the ugly paths.

5 min read Updated Sep 4, 2026
A Practical Guide to pepperQik Error Codes and Resilient Payment Flows

Payment integrations don't fail at the happy path — they fail at 18:47 on a Saturday when the terminal times out mid-purchase and the cashier has a queue of twelve. The difference between a POS that shrugs and a POS that loses money is how it treats error codes. pepperQik, like every serious EFT middleware, reports failures with structured codes; this guide is about turning that error catalogue into a strategy: classify, react, recover, reconcile.

Flow: error response is classified, then retried or reversed, then reconciled

Key takeaways

  • Every error belongs to one of four families: caller mistakes, business declines, transport failures, and unknown outcomes. Each family has exactly one correct reaction.
  • Retrying a decline is pointless; retrying an unknown outcome without a recovery query is dangerous.
  • Show cardholders and cashiers the middleware's display text — never your own creative rewording of a scheme decline.
  • An error you didn't log is an error you'll debug twice.

The four families of payment errors

FamilyExamplesCorrect reaction
1. Caller errors Invalid amount, unknown currency, operation while another is in flight, not logged in Fix the code. These are bugs in your POS, not conditions to handle at runtime.
2. Business declines Insufficient funds, card expired, PIN wrong, issuer says no Tell the cashier, offer another payment method. Never auto-retry.
3. Transport failures Terminal unreachable, connection refused, acquirer host down Retry with back-off before money moves; degrade gracefully (offline modes, cash) if it persists.
4. Unknown outcomes Timeout mid-transaction, service restart, cable pulled after authorization Recovery query first. Then reverse or complete based on the terminal's answer. This is the money-losing family.

A classification layer in code

Don't scatter if (errorCode == …) through your checkout. Centralize the mapping once:

public enum ErrorFamily { CallerBug, Declined, Transport, Unknown }

public static class PepperErrors
{
    // Illustrative mapping — drive it from the official error-code catalogue
    public static ErrorFamily Classify(TransactionError e) => e.Code switch
    {
        // caller mistakes: fail loudly in dev, alert in prod
        "INVALID_AMOUNT" or "ILLEGAL_STATE" or "NOT_LOGGED_IN"
            => ErrorFamily.CallerBug,

        // scheme / issuer said no: a *result*, not a failure
        "DECLINED" or "CARD_EXPIRED" or "PIN_TRIES_EXCEEDED"
            => ErrorFamily.Declined,

        // nothing financial happened yet
        "TERMINAL_UNREACHABLE" or "CONNECT_TIMEOUT" or "HOST_DOWN"
            => ErrorFamily.Transport,

        // something *may* have happened
        _ => ErrorFamily.Unknown,
    };
}

And one policy object that owns the reactions:

public async Task<SaleOutcome> HandleAsync(TransactionError error, EftPeer peer)
{
    switch (PepperErrors.Classify(error))
    {
        case ErrorFamily.CallerBug:
            log.Critical("POS bug talking to terminal: {Code}", error.Code);
            throw new PosIntegrationException(error);          // crash the flow, not the till

        case ErrorFamily.Declined:
            return SaleOutcome.Declined(error.DisplayText);    // verbatim to the cashier

        case ErrorFamily.Transport:
            return await retryPolicy.ExecuteAsync(             // e.g. 3 tries, 2s → 5s → 10s
                () => peer.RetryLastRequestAsync());

        case ErrorFamily.Unknown:
        default:
            var truth = await peer.QueryLastTransactionAsync(); // the recovery query
            return truth.WasApproved
                ? SaleOutcome.Approved(truth)                   // complete the sale
                : SaleOutcome.Failed(error);                    // safe to re-attempt
    }
}

The recovery query, again (because it's that important)

Family 4 exists because authorization and response travel separately: the issuer can approve while your timeout fires. If you re-run the purchase "to be safe", the customer pays twice; if you assume failure and hand over goods, you gave them away. The last-transaction query resolves the ambiguity — it asks the terminal for the definitive result of the most recent operation. Rules of engagement:

  1. Run it before any new financial operation after an unknown outcome, a POS crash, or a service restart.
  2. If the answer is "approved" but your basket was abandoned → issue a reversal (see EftPeer operations for reversal vs refund).
  3. If the terminal is unreachable, keep the lane blocked for cards and alert — do not guess.

Logging that wins disputes

When an acquirer chargeback lands three months later, this line is your defence:

2026-09-01T18:47:12+01:00 lane=04 ref=INV-000871 op=purchase amount=124.00
  outcome=UNKNOWN code=RESPONSE_TIMEOUT elapsed=61.2s
2026-09-01T18:47:14+01:00 lane=04 ref=INV-000871 op=last-tx-query
  outcome=APPROVED auth=834112 → sale completed

Structured, timestamped, reference-keyed, PAN-free. Ship these logs off the till nightly; a dead SSD should never take your evidence with it.

Testing the ugly paths

  • Pull the cable between "card inserted" and "response" — verify the recovery query path fires.
  • Kill the middleware mid-transaction — verify your POS refuses new payments until reconciled.
  • Simulate declines with test cards for the top decline codes — verify the cashier sees the terminal's text.
  • Run end-of-day with a mismatch injected — verify someone actually gets paged.

FAQ

Should I map error codes to user-friendly messages?

For cashier instructions, yes ("offer another card"). For the decline reason itself, surface the middleware/scheme text verbatim — it's what the issuer told the cardholder's bank, and rewording it creates support confusion.

How many retries for transport errors?

Two or three with increasing back-off, then degrade. A cashier watching a spinner for 90 seconds will pull the power cord and create a family-4 problem out of a family-3 one.

Where's the authoritative list of codes?

The pepperQik error-code reference — treat it as the source of truth and regenerate your classification table when you upgrade versions.

Next steps

Pair this with the API best-practices guide for the architectural side. And if your team is staring down a payment integration with a deadline, a short consulting engagement on the error model early is the cheapest insurance you'll ever buy.

Keep reading

Related articles