Skip to main content

Transaction Recovery & Status

This guide covers how to handle the case where your POS application misses a transaction result — either because the network was unavailable, the callback URL was unreachable, or the application restarted — and how to use the transactionReference to query the current status of any transaction and its full operation chain.


Why recovery matters​

When you send a transaction to POST /transactions, the terminal processes the payment autonomously. If your server is unreachable at the moment the result is delivered, you need a reliable way to retrieve the outcome later. Handpoint provides two complementary mechanisms:

  1. Terminal auto-recovery loop — the terminal automatically retries delivery to your callbackUrl
  2. Server-side polling — you poll GET /transaction-result/{transactionResultId} until the result arrives
  3. Status query by transactionReference — you query GET /transactions/{transactionReference}/status at any time to retrieve the current outcome

How transactionReference works​

transactionReference is a UUID v4 that you generate and include in original transaction requests (Sale, Remote Sale, Pre-Authorization, unlinked Refund). It acts as a stable key that:

  • Links all subsequent operations (Reversal, Capture, Refund) to the original transaction
  • Enables status queries at any time via the transactions.handpoint.com API
  • Returns all operations in the chain with status/all, so you can sum amounts to determine the net amount charged
Only on original transactions

Include transactionReference only on original operations. Do not send it on subsequent operations (Reversal, linked Refund, Pre-Auth Capture, Pre-Auth Void, Tip Adjustment). The gateway reads it from the original transaction and uses it to group subsequent operations automatically.

HiLite integration paths (Android HiLite, iOS HiLite) do not support transactionReference.

Original sale → you set transactionReference = "2bfde1fc-..."
└── Linked refund → no transactionReference (gateway links via originalTransactionId)
└── Tip adjustment → no transactionReference (gateway links via transactionID in URL)

1 — Terminal auto-recovery loop​

When you include a callbackUrl and token in your transaction request, the terminal posts the result to that URL on completion. If delivery fails, the terminal retries automatically:

WindowRetry interval
First 100 secondsEvery 5 seconds
After 100 secondsExponential backoff (8s → 16s → 32s → …)

The recovery loop restarts whenever the Handpoint application is restarted or startRecovery() is called (Android SDK). Results delivered via the recovery loop have "recoveredTransaction": true in the result body.

All 2xx HTTP response codes from your callbackUrl are valid acknowledgements. Respond with any 2xx status to stop retries.

Push-notification payload schema​

The terminal POSTs the full Transaction Result object as JSON to your callbackUrl. The body is identical to the 200 OK body from GET /transaction-result/{transactionResultId} — no fields are added or removed.

POST https://your-server.example.com/handpoint/callback
Content-Type: application/json
auth-token: <your-token-value>

{
"finStatus": "AUTHORISED",
"type": "SALE",
"totalAmount": 1000,
"currency": "USD",
"transactionID": "01236fc0-8192-11eb-9aca-ad4b0e95f241",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f",
"authorisationCode": "155884",
"cardSchemeName": "VISA",
"maskedCardNumber": "************1456",
"recoveredTransaction": false,
...
}

Two points specific to the push delivery:

  • token is in headers, not in the body. The token value from your original transaction request is echoed back as the auth-token HTTP request header (lowercase — HTTP/2 normalizes all header names to lowercase). Validate it server-side to verify the delivery is genuine.
  • recoveredTransaction is true when the result was delivered via the auto-recovery loop (the terminal could not reach your server on the first attempt). On first-attempt delivery it is false.

For the full list of fields in the Transaction Result object, see Transaction Result Object.


2 — Poll for result: GET /transaction-result/{transactionResultId}​

If you omit callbackUrl, the POST /transactions response includes a transactionResultId:

{
"transactionResultId": "0821032398-1628774190395",
"statusMessage": "Operation Accepted",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f"
}

Poll GET /transaction-result/{transactionResultId} until you receive a non-204 response:

GET https://cloud.handpoint.com/transaction-result/0821032398-1628774190395
ApiKeyCloud: YOUR_MERCHANT_API_KEY
ResponseMeaning
204 No ContentTransaction still in progress — retry
200 OKTransaction complete — body contains the full Transaction Result
404 Not FoundUnknown ID

Recommended polling interval: 3–5 seconds. The typical card-present transaction completes in 10–30 seconds.


3 — Query by transactionReference​

Use the transactions.handpoint.com API (note: different base domain from cloud.handpoint.com) to query the status of any transaction at any time using its transactionReference.

Different base URL

The status endpoint uses https://transactions.handpoint.com (production) or https://transactions.handpoint.io (staging) — not cloud.handpoint.com.

GET /transactions/{transactionReference}/status — first transaction​

Returns the current status of the first (original) transaction associated with the given transactionReference.

GET https://transactions.handpoint.com/transactions/2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f/status
ApiKeyCloud: YOUR_MERCHANT_API_KEY

Possible finStatus values:

StatusMeaning
AUTHORISEDTransaction approved
DECLINEDTransaction declined
REFUNDEDTransaction was refunded
IN_PROGRESSGateway received the transaction but outcome not yet known — retry in a few seconds
UNDEFINEDNot found in the gateway. The cardholder may still be interacting with the terminal (card presentation, PIN entry, mobile wallet CVM). A single transaction can take several minutes in worst-case fallback paths — see the worst-case timeline below. Continue polling until your active-recovery timeout expires before treating as a non-charge.

Example 200 response (abbreviated):

{
"type": "SALE",
"finStatus": "AUTHORISED",
"totalAmount": 1000,
"currency": "USD",
"transactionID": "01236fc0-8192-11eb-9aca-ad4b0e95f241",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f",
"authorisationCode": "155884",
"cardSchemeName": "VISA",
"maskedCardNumber": "************1456"
}

GET /transactions/{transactionReference}/status/{selector} — full chain​

Returns one or all operations linked to a transactionReference. Use this to see the complete lifecycle of a transaction and calculate the net amount.

GET https://transactions.handpoint.com/transactions/2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f/status/all
ApiKeyCloud: YOUR_MERCHANT_API_KEY

Selector values:

SelectorReturns
allAll operations linked to this transactionReference
firstThe original transaction
lastThe most recent operation
{n}The nth operation (1-based index)

Example — retrieving the full chain:

GET https://transactions.handpoint.com/transactions/2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f/status/all
ApiKeyCloud: YOUR_MERCHANT_API_KEY
[
{
"type": "SALE",
"finStatus": "AUTHORISED",
"totalAmount": 1000,
"transactionID": "01236fc0-8192-11eb-9aca-ad4b0e95f241",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f"
},
{
"type": "REFUND",
"finStatus": "AUTHORISED",
"totalAmount": 500,
"transactionID": "9a8b7c6d-1234-5678-9abc-def012345678",
"originalEFTTransactionID": "01236fc0-8192-11eb-9aca-ad4b0e95f241"
}
]

To determine the net amount on the merchant side, sum all AUTHORISED operations, subtracting REFUND and REVERSAL amounts:

Net = SALE(1000) - REFUND(500) = 500
finStatus interpretation

finStatus on the first transaction reflects its current state. On subsequent transactions it reflects the state at the time of processing. For example: a refund that was later reversed still shows AUTHORISED — it does not update to reflect the reversal.


4 — Worst-case terminal processing timeline​

Understanding how long the terminal can legitimately be processing helps you set an appropriate active-recovery timeout for your POS. All timeouts below are sourced from the Android SDK source (Constants.java, PendingMessageCommon.kt).

Android SDK cardholder screen timeouts​

Screen / PhaseDefault timeoutSource
"Tap, Insert or Swipe" (each attempt)30sGENERAL_CONFIG_CARD_READING_TIMEOUT
PIN entry (each attempt, PAX firmware)30sGENERAL_CONFIG_USER_INPUT_TIMEOUT → PAX library
Signature screen30sGENERAL_CONFIG_USER_INPUT_TIMEOUT
Application selection (multi-AID card)30sGENERAL_CONFIG_USER_INPUT_TIMEOUT
Partial approval confirmation30sGENERAL_CONFIG_USER_INPUT_TIMEOUT
Duplicate transaction confirmation30shardcoded DupeCheckMessageDialog
"See Phone" (mobile wallet on-device CVM)120sSEE_PHONE_TIMEOUT = 120 * 1000 ms

"See Phone" is triggered when a cardholder taps Apple Pay / Google Pay / Samsung Pay and the terminal requires the phone to complete biometric verification (Face ID, Touch ID, fingerprint) before the tap finalises. The terminal shows "Verification Required – Please check your mobile device" and waits up to 120s.

Retry counts (sourced from SDK)​

MethodAttemptsTotal card-read time
Contactless / NFC1 initial + 2 retries = 3 total (configurable)3 × 30s = 90s
Chip / EMV1 initial + 2 retries = 3 total (MAX_LIMIT_REACHED = 2)up to 3 × 30s = 90s
Magnetic stripe (swipe)1 attempt — no SDK retry loop30s window
PIN entry3 attempts (PAX firmware)up to 3 × 30s = 90s

Absolute worst-case chain (all fallbacks, all retries)​

Phase Time
─────────────────────────────────────────────────
Contactless: 3 attempts × 30s 90s
Chip fallback: 3 attempts × 30s 90s
Swipe fallback: 1 × 30s window 30s
PIN entry: 3 wrong attempts × 30s 90s
─────────────────────────────────────────────────
Terminal processing subtotal: 300s (5 min)

SDK GTS recovery (no-network): ~20s (exponential poll: 2s + 4s + 8s...)
Online authorisation (gateway): ~20s
─────────────────────────────────────────────────
Absolute worst case: ~340s (~5 min 40s)

This scenario (3 failed contactless + 3 failed chip + swipe + 3 wrong PINs) is extremely rare in practice. The typical worst case is chip + PIN: 3 × 30s + 3 × 30s + ~40s = ~220s (~3 min 40s).

Recommended active-recovery timeout: 6 minutes (360s) from request send time. After that, query /status once more — if still IN_PROGRESS or UNDEFINED, flag for manual reconciliation. Do not auto-retry with the same transactionReference.

See Phone path

Mobile wallet "See Phone" (120s) replaces part of the contactless phase and is a distinct path. If triggered, worst case becomes ~240s terminal processing + ~40s network = ~280s total — still within the 6-minute threshold.


5 — Android SDK: getTransactionStatus​

On Android PAX, use getTransactionStatus() to query the status from the SDK:

// Fetches current transaction status from the gateway using the transactionReference
// Result is delivered via your events listener
val success = hapi.getTransactionStatus("2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f")

The result is delivered asynchronously to your registered events listener. This is a PAX-only feature — not available on HiLite.


Best practices​

PracticeWhy
Generate a new UUID v4 transactionReference for every original transactionAvoids accidental deduplication; enables reliable status queries
Change transactionReference on every retry of the same transactionPrevents the gateway from treating a retry as the same operation
Store transactionReference in your database before sending the requestIf your app crashes mid-request, you can still query the outcome
Use /status/all for reconciliation, not individual /status callsIndividual calls only reflect the first transaction's current state
Stop active polling after 6 minutes (360s); query /status once more before flagging for manual reconciliationWorst-case terminal processing (all fallbacks + PIN retries) is ~340s — 360s gives a safe margin
On UNDEFINED after active-recovery timeout, treat as a non-chargeIf the terminal never reached the gateway within the timeout window, no charge was applied