Skip to main content

Testing Edge Cases

Step-by-step scenarios for verifying that your integration handles the full range of real-world edge cases.

Prerequisites​

RequirementNotes
Simulator merchantThe merchant must be configured against the Simulator (ViscusDummy) acquirer in Handpoint TMS — this is what enables trigger amounts, prevents real settlement, and allows expired cards. The device itself (production or debug hardware) does not determine this — only the merchant's acquirer configuration does. Contact your Handpoint integration engineer to have a test merchant provisioned.
Trigger amountsSpecific amounts that force a particular gateway response. Only work when the merchant is on the Simulator acquirer — see Development hardware.
Expired cardsAccepted on the Simulator acquirer — test cards can be past their expiry date.
transactionReference logA store (DB, log) where you record every UUID v4 you send before sending it.
Trigger amounts — quick reference

These amounts force specific responses on a ViscusDummy/Simulator merchant. All amounts are in minor currency units (e.g. 3784 = $37.84 if sending USD).

AmountForced finStatusstatusMessage
3779DECLINEDRefer to card issuer
3784DECLINEDTransaction failed
3793DECLINEDPick-up card
3757PARTIAL_APPROVALApproved (partial)
3768FAILEDError connecting to authorization provider
3741FAILEDProcessing error

Any other amount: AUTHORISED (normal approval). → Full table including cancellation amounts: Development hardware


Sale​

Declined transaction​

  1. Send POST /transactions with a trigger amount for DECLINED.
  2. Verify the response body contains "finStatus": "DECLINED".
  3. Confirm your POS surfaces the decline message to the operator.
  4. Verify no charge appears in your test merchant portal.

Network drop — recover via /status​

Tests your ability to recover a transaction result when your server missed the callback.

  1. Generate a UUID v4 transactionReference and write it to your database before sending the request.
  2. Send POST /transactions with that transactionReference.
  3. Immediately after sending — before the result arrives — disconnect your server from the network or stop your callback listener.
  4. Wait 15 seconds, then reconnect.
  5. Query GET https://transactions.handpoint.com/transactions/{transactionReference}/status.
  6. Expect IN_PROGRESS while the terminal processes, then AUTHORISED or DECLINED once complete.
  7. Verify your reconciliation flow correctly records the result from the status endpoint.
  8. Verify you do not send a second transaction with the same transactionReference during the wait.

Duplicate transactionReference (idempotency)​

  1. Send POST /transactions with transactionReference = "your-test-uuid". Note the result.
  2. Send the exact same request again with the same transactionReference.
  3. Verify the gateway returns the original result — not a second charge.
  4. Verify only one charge appears in your test merchant portal.

Card fallback path — contactless → chip → swipe​

Tests that the full fallback chain works end-to-end and that your app handles the extended time (~2–5 min) without timing out or sending a duplicate request.

Card fallback is handled entirely by the terminal. From the Cloud API side:

  1. Send POST /transactions with transactionReference.
  2. Do not poll or retry — wait for the result via callbackUrl or GET /transaction-result/{id}.
  3. Verify your polling loop stays active for at least 6 minutes without sending a second request.
  4. On the terminal, allow the fallback chain to complete (tap fail × 3 → chip fail × 3 → swipe success).
  5. Verify the result arrives and is accepted by your callback handler.

Worst-case timeout — 6-minute integration test​

Verifies your POS does not give up before the terminal has finished.

  1. Send POST /transactions with transactionReference. Start a stopwatch.
  2. On the terminal, deliberately take the longest path (3 contactless fail → 3 chip fail → swipe → 3 wrong PINs → correct PIN).
  3. Verify your polling/callback listener is still active at 5 minutes.
  4. Verify the result is accepted when it arrives (anywhere up to ~6 minutes after send time).
  5. Verify GET /status returns IN_PROGRESS throughout and AUTHORISED after completion.

Refund​

Linked refund — same card (card-present paths)​

  1. Complete a sale — note the transactionID from the result.
  2. Send POST /transactions with "operation": "refund" and "originalTransactionId": "{transactionID}".
  3. On the terminal, present the same card used for the original sale.
  4. Verify finStatus: AUTHORISED.

Linked refund — mismatched payment method​

Tests the physical-card vs mobile-wallet PAN token mismatch. Requires a merchant configured for same-card verification.

  1. Complete a sale using a physical card (chip insert) — note transactionID.
  2. Initiate a linked refund with originalTransactionId.
  3. On the terminal, present the same account via Apple Pay or Google Pay (wallet tap).
  4. Verify the refund is declined — wallet generates a different PAN token than the physical card.
  5. Repeat in reverse: original sale via wallet, refund via physical insert — same decline expected.
  6. Verify your UI instructs the cardholder to use the same payment method as the original purchase.

Refund amount exceeds original​

  1. Complete a sale for 1000 (minor units).
  2. Send a linked refund with amount: "1500" and originalTransactionId.
  3. Verify the gateway declines with AMOUNT_EXCEEDS_ORIGINAL.

Reversal​

Before cut-off — no card required​

  1. Complete a sale — note transactionID.
  2. Within the same business day (before batch cut-off), send POST /transactions with "operation": "saleReversal" and "originalTransactionId".
  3. Verify no card interaction is required on the terminal.
  4. Verify finStatus: AUTHORISED (reversal approved, hold released).
  5. Verify no charge appears in your test merchant portal.

After cut-off — should fail​

  1. Complete a sale — note transactionID.
  2. Manually close the batch (or wait until after the scheduled cut-off time).
  3. Attempt a reversal with the same originalTransactionId.
  4. Verify the error ORIGINAL_NOT_FOUND or equivalent batch-closed error.
  5. Verify your POS routes the operator to a post-settlement Refund flow instead.

Pre-Authorization lifecycle​

Full lifecycle: create → capture​

  1. Send POST /transactions with "operation": "preAuthorization" and a transactionReference. Note the transactionID from the result.
  2. Verify finStatus: AUTHORISED (hold placed — no funds captured).
  3. Send POST /preauthorization/capture with { "originalGuid": "{transactionID}", "capturedAmount": "{amount}" }.
  4. Verify capture result finStatus: AUTHORISED.
  5. Query GET /transactions/{transactionReference}/status/all — verify two operations appear (Pre-Auth + Capture).

Create → void (release hold without capturing)​

  1. Send a Pre-Auth create — note transactionID.
  2. Send POST /transactions with "operation": "preAuthorizationReversal" and "originalTransactionId".
  3. Verify finStatus: AUTHORISED (hold released).
  4. Verify no amount was captured in your test merchant portal.

Signature (Android PAX only)​

Single 30-second window — clearing does not reset the timer​

Signature is a terminal-side CVM — not applicable for Cloud API testing directly. Test via Android PAX.


Mobile wallet — "See Phone" (120-second window)​

Triggered when Apple Pay / Google Pay / Samsung Pay requires on-device biometric verification (Face ID, Touch ID, fingerprint) before the tap is finalised. The terminal shows "Verification Required – Please check your mobile device" and waits up to 120 seconds.

  1. Send POST /transactions with transactionReference.
  2. On the terminal, tap with a mobile wallet card that triggers on-device CVM.
  3. When the "See Phone" screen appears, do not complete biometric auth — wait and observe.
  4. Verify the screen times out after 120 seconds and the transaction is declined/cancelled.
  5. Verify your callbackUrl receives the result within your 6-minute recovery window.

transactionReference rules — validation checklist​

Run these on any integration path that supports transactionReference (Cloud API only; not applicable on HiLite paths).

CheckExpected result
Send transactionReference on a SaleAccepted; queryable via /status
Send transactionReference on a Remote SaleAccepted; queryable via /status
Send transactionReference on a Pre-Auth createAccepted; links all lifecycle operations
Send transactionReference on an unlinked RefundAccepted; queryable via /status
Send transactionReference on a ReversalShould be ignored or rejected — do not send
Send transactionReference on a linked RefundShould be ignored or rejected — do not send
Send transactionReference on a Pre-Auth CaptureShould be ignored or rejected — do not send
Send transactionReference on a Pre-Auth VoidShould be ignored or rejected — do not send
Query /status/all after a Sale + RefundBoth operations appear in the array
Query /status/all after a Sale + ReversalBoth operations appear in the array
Query /status immediately after sending (terminal still processing)IN_PROGRESS
Query /status after result deliveredAUTHORISED or DECLINED
Query /status with a random UUID that was never sentUNDEFINED

EMV Forced Reversal — card removed mid-chip​

When a chip card is removed from the reader after the gateway has authorized the transaction but before the full EMV flow completes (or when the card's internal application declines after online authorization), the SDK sends a forced-reversal to release the hold and then delivers a DECLINED result.

The dangerous moment: /status briefly shows AUTHORISED while the forced-reversal is in flight. An integration that saves the /status result immediately will record an incorrect approved transaction that was actually reversed.

This edge case is easy to reproduce on real hardware — no trigger amount required.

  1. Send POST /transactions with "operation": "sale" and any non-trigger amount. Note the transactionResultId.
  2. On the terminal, insert the chip card.
  3. As soon as the terminal shows "Processing..." or "Please wait" (after PIN entry), quickly pull the card out of the reader.
  4. Immediately poll GET /transaction-result/{transactionResultId} — you may see finStatus: AUTHORISED briefly.
  5. Do not record this result. Continue polling.
  6. Within 5–30 seconds, the result changes — the final transaction-result will have finStatus: DECLINED and statusMessage similar to "card declined the online authorization."
  7. Verify your integration records DECLINED, not the intermediate AUTHORISED.

What this tests: That your polling loop waits for transaction-result delivery rather than acting on the first /status poll.

What to tell the cardholder

Display the statusMessage to the merchant. The standard instruction is: re-insert the card and leave it in the reader until the terminal confirms completion. Do not remove the card during processing.


Partial Approval (US only)​

US acquirers only

Partial approvals only occur on US acquirer configurations (EPI, Interac). The trigger amount 3757 is only active when the merchant is provisioned on the Simulator acquirer with a US MCC. Do not test this on EU or non-US configurations.

Accept flow — cardholder accepts the partial amount​

  1. Send POST /transactions with "amount": "3757" and "currency": "USD". Save the transactionResultId.
  2. The terminal will display a partial approval prompt (e.g. "Approved $11.00 of $37.57 — Accept?").
  3. While the prompt is active, GET /transactions/{ref}/status returns finStatus: AUTHORISED — this is not final. The amount fields reveal the partial: totalAmount < requestedAmount and dueAmount > 0. Do not save the AUTHORISED result.
  4. On the terminal, press Accept.
  5. Poll GET /transaction-result/{id} — the final result shows finStatus: PARTIAL_APPROVAL with totalAmount as the approved partial amount.
  6. Verify your integration records totalAmount as the settled amount — not requestedAmount.
  7. Verify your POS prompts for split tender (remaining requestedAmount − totalAmount) or surfaces a "partial payment accepted" message.

Polling requirement: Continue polling transaction-result for at least 60 seconds — the accept/decline prompt on the terminal can take up to 60 seconds to resolve. From /status, use dueAmount > 0 as a secondary signal that the partial approval prompt is still active.


Decline flow — cardholder declines the partial amount​

  1. Send POST /transactions with "amount": "3757" and "currency": "USD".
  2. On the terminal, press Decline when the partial approval prompt appears.
  3. The SDK automatically sends a reversal for totalAmount — no action from your integration is required.
  4. Continue polling GET /transaction-result/{id} — the final result must show finStatus: CANCELLED.
  5. Verify your integration does not save the transaction as a sale.
  6. Verify your POS prompts the cardholder to use a different payment method.
  7. Optionally: query GET /transactions/{transactionReference}/status/all and verify the array contains two entries — the CANCELLED sale and an AUTHORISED reversal.

Key verification: While the prompt was active, /status showed AUTHORISED with dueAmount > 0. If your integration saved that AUTHORISED as a completed sale before the decline resolved, this test will expose that bug. The final transaction-result is the authoritative outcome.


ISV reversal — integration does not accept partial approvals​

If your integration policy is to never accept partial approvals, test the reversal path:

  1. Send POST /transactions with "amount": "3757".
  2. On the terminal, press Accept to produce a finStatus: PARTIAL_APPROVAL result with a transactionID.
  3. Immediately send POST /reversal with { "originalGuid": "{transactionID}" } (no amount needed for a full reversal).
  4. Verify the reversal finStatus: AUTHORISED.
  5. Verify no net charge appears in your test merchant portal (the hold is released).
  6. Verify your POS shows "Insufficient funds on this card" and prompts for an alternative payment method.
Use totalAmount if sending amount on the reversal

The reversal endpoint requires the amount that was actually authorized — totalAmount from the partial approval result — not requestedAmount. If your acquirer requires an explicit amount, use totalAmount. Using requestedAmount will fail or cause a settlement mismatch.