Skip to main content

Error Handling Guide

This guide explains how to handle errors in your integration. For the complete code-level reference — specific error codes, HTTP statuses, and error shapes — see Error codes.


Error delivery: three patterns​

Which error pattern an endpoint uses determines where you look for the error.

PatternEndpointsWhere the error is
A — AsyncPOST /transactions (all card-present)Poll GET /transaction-result/{id} → read finStatus and statusMessage
B — Sync flat/reversal, /capture, /increase, /moto/sale, /moto/refund, /tip-adjustmentHTTP 4xx response → read error.code
C — Sync nestedGET /transactions/{id}/tokenHTTP 400 → read error.details.body.error.errorCode

See Error codes — three patterns for the exact JSON shapes.


Gateway errors vs acquirer responses​

Not all DECLINED transactions are the same. Understanding the source of a decline determines how your application should respond.

Gateway errors​

Set by the Handpoint gateway before the transaction reaches the acquirer. These indicate a configuration or connectivity problem — the acquirer was never contacted.

How to identify: finStatus is DECLINED or FAILED and the error is clearly non-issuer (see table below). These are actionable by the ISV or merchant.

statusMessagearcSourceWhat it means
Invalid Merchant1000GatewayexternalId in merchantAuth doesn't match any configured sub-MID
Error connecting to authorization provider—GatewayTimeout reaching the acquirer — no authorization attempt was made
Processing error—GatewayInternal processing failure before acquirer contact
Read card error—GatewayTerminal could not read the card (chip, contactless, or magstripe read failed)

Acquirer responses (pass-through)​

Set by the card issuer or acquirer after the transaction is processed. The statusMessage is a localized string forwarded from the network — its exact text varies by card locale.

The ISV's responsibility is to display the statusMessage to the merchant and let them decide how to proceed. Do not translate or override acquirer messages in your application logic — the merchant knows their business context (e.g. "Pick-up card" requires a specific response from trained staff).

Display statusMessage as-is

statusMessage is already localized to the cardholder's card language. Display it to the merchant without modification. Use finStatus for any programmatic branching in your code.


Handling each finStatus​

finStatus is the top-level outcome field on every polled result (GET /transaction-result/{id}). Use it for all programmatic branching — not statusMessage, which is localized and varies by card locale.

finStatusFinal?MeaningRecommended ISV action
IN_PROGRESSNoStill processing on the terminal or gatewayKeep polling. Do not act.
UNDEFINEDNo*Terminal sent the transaction but no result was receivedDo not retry. Follow the UNDEFINED recovery flow. *Treat as non-final until the recovery flow resolves it.
AUTHORISEDYes‡ApprovedWait for transaction-result delivery before recording as final — see note below.
DECLINEDYesDeclined by gateway or acquirerDisplay statusMessage to the merchant. No silent retry — only retry after cardholder action (new card, contact bank).
FAILEDYesTechnical failure — acquirer may or may not have been reachedDo not retry without recovery. Follow the recovery flow first to determine if the transaction completed.
PARTIAL_APPROVALNo†Approved for less than the requested amount (US only). Terminal is still showing an accept/decline prompt — result can change.Do not save as final. Continue polling transaction-result for at least 60 s or until a final result is delivered. If cardholder declines, the SDK auto-reverses and the result changes to CANCELLED. If accepted, present the partial amount and offer split tender for the remainder.
CANCELLEDYesCardholder or merchant cancelled at terminalNo action required. Present the checkout UI again if needed.
PROCESSEDYesCompleted for non-financial operations (tokenization, tokenizeCard)Record the result. No further action needed.
REFUNDEDYesRefund processed successfullyRecord the result. Do not retry.
CAPTUREDYesPre-authorization capturedRecord the result. Do not retry.

*UNDEFINED — treat as non-final until the recovery flow resolves it.

‡AUTHORISED — in two specific edge cases, /status shows AUTHORISED while the SDK is still running a forced-reversal in the background; the final transaction-result will be DECLINED:

  1. Card removed mid-chip — the chip card was pulled from the reader after the gateway authorized but before the full EMV flow completed. The SDK sends a forced-reversal and delivers a DECLINED result with statusMessage similar to "card declined the online authorization."
  2. Internal card application decline — the card's on-chip application rejected the transaction after gateway authorization (e.g. IAD mismatch or internal card logic). Same forced-reversal flow. In both cases the fix is the same: never save a /status AUTHORISED as final — always wait for transaction-result delivery or poll for at least 60 s.

†PARTIAL_APPROVAL — the terminal is showing an accept/decline prompt to the cardholder. The result is not final. Continue polling transaction-result for at least 60 seconds, or until a final result is delivered. If the cardholder declines, the SDK auto-reverses and the result changes to CANCELLED. See Partial Approval.


Setup and configuration errors​

These occur when the merchant account or request is misconfigured — the terminal or gateway rejects the request before any card processing begins.

Device connectivity​

SymptomHTTPErrorFix
Terminal offline400error 1002: No device listeningCheck device is powered on, connected to Wi-Fi, and the Handpoint Payments App is open
Terminal busy400error 1001: Device is busyWait 2–5 seconds and retry; another operation is in progress
Terminal not assigned400error 1004: Auth not availableVerify the terminal serial is assigned to this merchant in Handpoint Portal; use GET /devices to list valid serials

Request validation​

SymptomHTTPErrorFix
Unknown field in request body422must NOT have additional propertiesRemove the unrecognised field; check the field name against the API spec (e.g. externalId at top level is invalid — use merchantAuth: [{ "externalId": "..." }])
transactionReference not UUID v4400TransactionReference with wrong uuidv4 formatGenerate a valid UUID v4 — version digit (position 13) must be 4, variant digit (position 17) must be 8, 9, a, or b
Wrong API key403No valid key found in headerCheck the ApiKeyCloud header value

Merchant capability errors​

Capability restrictions produce different error shapes depending on which layer enforces them.

Enforcement tiers​

CapabilityEnforcementImmediate HTTPFinal outcomeHow to detect
preAuthAllowedTerminal202finStatus: DECLINEDPoll result
refundAllowedTerminal202finStatus: DECLINEDPoll result
partialReversalAllowedGateway400— (synchronous)error.code: "3109"
supportsMotoTerminal202finStatus: FAILEDPoll result + errorMessage
cardTokenProviderTerminal202finStatus: DECLINEDPoll result; transactionID is empty string
debitCardsOnlyTerminal / Acquirer202finStatus: DECLINEDPoll result (real acquirers only — see below)

Terminal-enforced: gateway returns 202, terminal reads the card, then declines. Poll GET /transaction-result/{transactionResultId} for the final result.

Gateway-enforced (partialReversalAllowed only): gateway rejects synchronously with HTTP 400. No card is read; no polling needed.

Per-capability error examples​

Pre-auth not enabled — preAuthAllowed = false

// Poll result
{
"finStatus": "DECLINED",
"statusMessage": "Pre-authorizations are not enabled for this terminal",
"arc": "1000",
"cardEntryType": "ICC",
"type": "PRE_AUTHORIZATION",
"issuerResponseCode": "00"
}

issuerResponseCode: "00" is a placeholder — the transaction did not reach the issuer. statusMessage is locale-dependent.

Refund not enabled — refundAllowed = false

// Poll result
{
"finStatus": "DECLINED",
"statusMessage": "Refund not allowed",
"arc": "0000",
"cardEntryType": "ICC",
"type": "REFUND"
}

TSYSDummy rejects refunds synchronously with HTTP 400 when refundAllowed=false. ViscusDummy forwards to the terminal (202) and declines after card read. Handle both paths.

Partial reversal not enabled — partialReversalAllowed = false

// HTTP 400 — synchronous, no terminal involvement
{
"error": {
"statusCode": 400,
"name": "BadRequestError",
"message": "Partial reversals are not supported",
"code": "3109",
"details": { "errorCode": "3109", "reason": "Partial reversals are not supported" }
}
}

MOTO not enabled — supportsMoto = false

// Poll result — finStatus is FAILED, not DECLINED
{
"finStatus": "FAILED",
"statusMessage": "HMAC mismatch",
"errorMessage": "HMAC mismatch",
"paymentScenario": "MOTO",
"cardEntryType": "CNP",
"type": "MOTO_SALE"
}

FAILED (not DECLINED) — ViscusDummy returns a technical error when MOTO is disabled. Real acquirers may return DECLINED. Always branch on finStatus.

Tokenization not enabled — cardTokenProvider = null

// Poll result — acquirer was never contacted
{
"finStatus": "DECLINED",
"statusMessage": "Card token failure",
"cardToken": "",
"transactionID": "",
"requestedAmount": 0,
"type": "SALE"
}

Empty transactionID and requestedAmount: 0 signal that the acquirer was never reached — the tokenization step failed before authorization.

Known limitation — debitCardsOnly​

ViscusDummy and TSYSDummy do not enforce debitCardsOnly at the acquirer level. Credit card transactions return AUTHORISED in test environments. This restriction only takes effect on real acquirer configurations — do not validate this capability using dummy acquirers.


Setup-related declines are not integration errors. The ISV should always return the statusMessage to the merchant and keep request/response logs from the Cloud API for troubleshooting. The merchant escalates to their onboarding partner — not to Handpoint Integration Support. Handpoint Integration Support handles integration issues only (SDK behaviour, API contract questions, connectivity).


Diagnosing an unexpected decline​

When a transaction declines unexpectedly, work through these checks in order:

  1. Is finStatus = DECLINED and arc = 1000? → Gateway error (likely setup). Check setup errors.
  2. Is finStatus = FAILED? → Technical failure. Do not retry — follow the recovery flow.
  3. Does statusMessage look like an issuer message? (e.g. "Insufficient funds", "Expired card", "Refer to card issuer") → Acquirer pass-through. Display as-is to the merchant.
  4. Is finStatus = UNDEFINED? → Follow the UNDEFINED recovery flow.
  5. Did you receive HTTP 422? → Request validation failed. Read error.details for the specific field that was rejected.

Retry policy​

finStatusRetry?Notes
AUTHORISEDNeverAlready approved
DECLINED (issuer)Only with cardholder actionNew card, contact bank — not a silent retry
DECLINED (gateway, setup)After fixing configRetry only after resolving the underlying setup issue
FAILEDOnly after recovery confirms no resultFollow recovery flow first
UNDEFINEDOnly after recovery confirms no resultFollow recovery flow first
CANCELLEDSafe to retryCardholder chose to cancel; present checkout again
PARTIAL_APPROVALNever the full amountHandle the partial amount; accept or send reversal
PROCESSEDNeverNon-financial operation completed
REFUNDEDNeverAlready refunded
CAPTUREDNeverPre-auth already captured
Never retry FAILED or UNDEFINED without recovery

Both FAILED and UNDEFINED mean the acquirer may have processed the transaction without your client receiving a result. Retrying without checking creates duplicate charges.