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.
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.
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
| Family | Examples | Correct 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:
- Run it before any new financial operation after an unknown outcome, a POS crash, or a service restart.
- If the answer is "approved" but your basket was abandoned → issue a reversal (see EftPeer operations for reversal vs refund).
- 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.