Skip to main content

Validate your integration

Before going live, every Handpoint integration must pass a set of mandatory validation scenarios. The required tests depend on your integration path and the operations you have enabled.

Certification process

Handpoint Integration Support reviews your results before issuing a production API key or merchant credentials. Run all required scenarios on a staging/DEMO merchant and document the outcomes. Contact your integration engineer to schedule a certification call.

SDK-specific validation pages

Android SDK (PAX) integrations have additional requirements: build process (RC vs production SDK), PAX Store submission, merchant credential mapping, UI screen requirements, logging, and receipt handling. See the Android SDK — Validation & Certification page for the full checklist.

Before you start: test your edge cases first

The Testing Edge Cases page has step-by-step scenarios (declined, network drop, duplicate reference, card fallback, partial approval, MOTO timeout) with per-platform tabs. Run those before working through this checklist.

Required for every integration​

These scenarios apply regardless of integration path or acquirer.

Transaction recovery on connection loss​

What to test: Simulate a connection drop mid-transaction. Your software must:

  1. Detect that no result was received (timeout, socket close, app backgrounded/killed).
  2. On reconnection, query the transaction status.
  3. If the outcome is unknown (UNDEFINED or no result), send an automatic reversal to avoid double-charges.
  4. Only present the "approved" or "declined" result to the cashier once you have a definitive outcome.

Why it is required: A transaction that disappears mid-flight may have been authorised at the gateway. If you do not reverse it and the cardholder is not charged, you take a loss. If you retry without checking, you risk double-charging the cardholder.

Step 1 — Persist state before sending (required)

Persist the transactionReference to durable storage (database or localStorage) before sending POST /transactions. If the page refreshes or the app crashes after the request is sent but before the result arrives, this is the only way to recover.

// Generate and persist BEFORE the network call
const txnRef = crypto.randomUUID();
const pendingState = {
transactionReference: txnRef,
transactionResultId: null, // filled after 202
startedAt: Date.now(),
amount: '1000',
currency: 'USD'
};
localStorage.setItem('hp_pending_txn', JSON.stringify(pendingState));

// Send the transaction
const resp = await fetch('https://cloud.handpoint.com/transactions', {
method: 'POST',
headers: { 'ApiKeyCloud': API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ operation: 'sale', amount: '1000', currency: 'USD',
terminal_type: 'PAXA920', serial_number: '082104578',
transactionReference: txnRef })
});
const { transactionResultId } = await resp.json(); // HTTP 202

// Update state with resultId so we can resume polling after a crash
pendingState.transactionResultId = transactionResultId;
localStorage.setItem('hp_pending_txn', JSON.stringify(pendingState));

Step 2 — Poll with correct 204/200 handling

async function pollResult(transactionResultId, maxAttempts = 30, intervalMs = 4000) {
for (let i = 0; i < maxAttempts; i++) {
await new Promise(r => setTimeout(r, intervalMs));
const resp = await fetch(`https://cloud.handpoint.com/transaction-result/${transactionResultId}`,
{ headers: { 'ApiKeyCloud': API_KEY } });

if (resp.status === 204) continue; // Still processing — do NOT call .json()
if (resp.status === 200) return await resp.json(); // Result ready
throw new Error(`Unexpected status ${resp.status}`);
}
return null; // Timeout — trigger recovery
}

Step 3 — On page load, check for a pending transaction

async function recoverOnStartup() {
const raw = localStorage.getItem('hp_pending_txn');
if (!raw) return; // No pending transaction

const pending = JSON.parse(raw);

// Option A: We have a resultId — resume polling first
if (pending.transactionResultId) {
const result = await pollResult(pending.transactionResultId);
if (result) {
handleFinalResult(result);
localStorage.removeItem('hp_pending_txn');
return;
}
}

// Option B: Poll timed out or we never got a resultId — query /status
const statusResp = await fetch(
`https://transactions.handpoint.com/transactions/${pending.transactionReference}/status`,
{ headers: { 'ApiKeyCloud': API_KEY } }
);
const status = await statusResp.json();

if (status.finStatus === 'AUTHORISED') {
// ⚠️ If this could be a partial approval, wait 60s before saving — see partial approval note below
handleFinalResult(status);
} else if (status.finStatus === 'IN_PROGRESS') {
// Still running — resume polling if you have a resultId, or wait and re-query /status
scheduleStatusRetry(pending);
} else if (status.finStatus === 'UNDEFINED' || !status.finStatus) {
// Unknown outcome — auto-reverse to avoid double-charge risk
await fetch('https://cloud.handpoint.com/reversal', {
method: 'POST',
headers: { 'ApiKeyCloud': API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ originalGuid: status.transactionID || pending.transactionReference })
});
} else {
// DECLINED, CANCELLED, FAILED — no charge
}
localStorage.removeItem('hp_pending_txn');
}
Partial approval /status timing trap

If /status returns finStatus: AUTHORISED and totalAmount < requestedAmount, the cardholder may still be deciding on-terminal whether to accept or cancel the partial amount. Do not save this as a final result immediately. Wait 60 seconds for the normal transaction-result polling to deliver the true final result. If no transaction-result arrives in that window, re-query /status — the cardholder's decision will then be reflected. See Partial Approvals for full details.

Step 4 — Simulate failures

  1. Send POST /transactions and kill the browser tab immediately after the 202 response.
  2. Reopen the tab — your recoverOnStartup() should detect the pending state in localStorage.
  3. Verify it correctly resolves to AUTHORISED, DECLINED, or initiates a recovery reversal.
  4. Verify localStorage is cleared after resolution — no orphaned pending state.

App timeout and terminal idle handling​

Handpoint does not enforce a global operation timeout — your application must implement one.

Required behaviour:

  • Set a maximum wait time per operation appropriate to your UX (typical: 90–120 seconds for card-present, 60 seconds for back-office REST calls).
  • On timeout: do NOT silently abandon the transaction. Follow the transaction recovery flow above to determine the outcome before clearing the UI.
  • When the terminal is idle between transactions, ensure no orphan operation is pending. The terminal will reject new operations with Device is busy (1001) until the previous one completes or times out.
Integration pathHow to implement timeout
Cloud APIUse an HTTP client timeout for the initial POST. For polling, set a max poll duration and retry interval (recommended: poll every 2s, give up after 90s, then reverse).
Android SDKImplement a countdown in your UI thread. Do not call a second operation until endOfTransaction fires. Android SDK integrations must apply a 7-minute timeout.
iOS SDKUse a DispatchWorkItem or Timer. Cancel and reverse if no delegate response within the timeout window.
CordovaUse setTimeout in JavaScript wrapping the handpoint.* call.
JavaScript SDKWrap transactionResult with Promise.race([transactionResult, new Promise((_, rej) => setTimeout(rej, 90000))]). On timeout, call hp.getTransactionStatus() before clearing the UI.
Windows SDKUse a System.Timers.Timer or CancellationTokenSource. On expiry, do not cancel the SDK operation — call hapi.GetTransactionStatus() to determine outcome, then reverse if needed.

Partial approval handling​

Partial approval (PARTIAL_APPROVAL) is a US-only feature that is enabled by default. Every US integration will encounter it in production. Handling it correctly is required for Handpoint integration certification — it is tested using trigger amount 3757 (minor units).

Some prepaid and debit cards are approved for less than the requested amount. The terminal presents an accept/decline prompt to the cardholder.

ISV options:

Option 1 — Accept partial approvals (required if your MCC mandates it — consult your acquirer):

  • Detect finStatus === "PARTIAL_APPROVAL".
  • Fulfil the order at totalAmount. Display totalAmount on the receipt.
  • Prompt for the remaining dueAmount via a second payment method.

Option 2 — Do not support partial approvals:

  • Detect finStatus === "PARTIAL_APPROVAL".
  • Immediately reverse using totalAmount (the authorized amount) — never requestedAmount.
  • Display "Insufficient funds — transaction cancelled" or equivalent.
  • Log both transactions in your history: the original PARTIAL_APPROVAL sale and the reversal. Both receipts must be accessible.
  • Prompt for an alternative payment method.
Wait 60 seconds before reversing a partial approval

After a partial approval, the terminal is showing the accept/decline prompt. The cardholder has up to 60 seconds to decide. If the cardholder declines, the SDK auto-reverses and the final result is CANCELLED — your reversal request may then fail as already-reversed (error 3051). Wait for a final transaction-result before sending your own reversal.

→ Full implementation details: Partial Approval guide


Merchant API key management​

ApiKeyCloud is unique per merchant account. Your integration must maintain a mapping from your internal merchant ID to the corresponding Handpoint API key. Never use a single shared API key across multiple merchant accounts.

RequirementDetail
Per-merchant key storageStore each merchant's API key in your backend, keyed to your merchant identifier. Do not store in frontend code or mobile app bundles.
Staging vs productionStaging keys (cloud.handpoint.io) and production keys (cloud.handpoint.com) are separate. Ensure your environment config switches the key and the base URL together.
Secure storageAPI keys must not be logged in plain text, committed to source control, or returned to frontend clients. Treat them as secrets — store in a secrets manager or encrypted config.
Key rotationIf a key is compromised, contact Handpoint Support immediately for rotation.

When onboarding a new merchant: receive their ApiKeyCloud from Handpoint (or via your portal integration), store it against the merchant record, and test with a DEMO/staging merchant before going live.


Receipt compliance — EMV card scheme requirements​

Card schemes (Visa, Mastercard, Discover, Amex) require that a receipt be available to the cardholder on demand for every EMV transaction. Delivery method is your choice — email, SMS, printed receipt, or an in-app receipt screen.

Required fields​

Include the following in every customer receipt. Fields marked conditional are required only when present in the transaction result (non-empty, non-null).

FieldSource in resultNotes
Date and timeterminalDateTime (local) or serverDateTime (UTC)Show in cardholder's local time
Transaction typetype (SALE, REFUND, REVERSAL, etc.)
OutcomefinStatus + statusMessage (issuer text)
Amount chargedtotalAmount (not requestedAmount)Use totalAmount — on partial approvals these differ
CurrencycurrencyISO 4217 code
Card schemecardSchemeName or cardTypeNamee.g. "Visa", "Mastercard"
Masked card numbermaskedCardNumberLast 4 digits minimum
Authorisation codeauthorisationCodeRequired for disputes
Issuer responseissuerResponseCode + issuerResponseTexte.g. "00 / Successful"
Transaction IDtransactionIDRequired for support escalation
Retrieval referenceretrievalReferenceNumberRequired for chargebacks
AIDapplicationIdentifierConditional — EMV chip only; absent for contactless/swipe
TVRtvrConditional — EMV chip only
IADiadConditional — EMV chip only
ARCarcConditional — only include if non-empty
Merchant nameYour merchant recordFull legal name
Merchant addressYour merchant recordFull address
MIDacquirerMid or your merchant recordMerchant ID at acquirer
TIDacquirerTidTerminal ID at acquirer
transactionReferencetransactionReferenceFor troubleshooting — suggested, not mandatory
Serial numberYour configSuggested for troubleshooting — links to device if disputed

Receipt URL vs raw HTML​

The merchantReceipt and customerReceipt fields in the transaction result contain either:

  • A hosted URL (e.g. https://receipts.handpoint.com/receipts/{id}/customer.html) — fetch and display or link in email/SMS
  • Raw HTML string — rendered directly if the terminal could not upload to the receipt server (MOTO on-terminal, network issues)

Your code must handle both. Check whether the value starts with http — if yes, fetch; if no, render as HTML.

Known issue — recovered transactions​

No receipt for recovered transactions

When a transaction is recovered via GET /transactions/{transactionReference}/status (after a network failure or app crash), the result does not include merchantReceipt or customerReceipt URLs. You must build the receipt yourself from the fields in the /status response using the table above.

Testing receipt delivery​

ScenarioExpected
Standard card-present salemerchantReceipt and customerReceipt fields present and contain a URL or HTML
MOTO on-terminal saleReceipts delivered as raw HTML strings (no URL)
Recovered transaction via /statusNo receipt fields — ISV-built receipt required
Email/SMS deliveryCardholder receives receipt link within 30 seconds
Printed receiptAll required EMV fields printed; no truncation

Logging requirements​

Handpoint Integration Support requires access to request/response logs to assist with dispute resolution and certification. Your integration must:

RequirementDetail
Log all API requestsURL, HTTP method, request body (redact card numbers), timestamp
Log all API responsesHTTP status code, response body, latency
Log transaction referencesEvery UUID v4 you generate — before sending — with timestamp and merchant ID
Minimum retention2 weeks (14 days). Longer is better for dispute resolution.
API key handlingLog the header name (ApiKeyCloud) but not the key value — treat it as a secret
CorrelationEach log entry must be linkable to the transactionReference and transactionResultId for the same operation

Logs should be available to your team on request during a support case. They are not automatically shared with Handpoint — you submit relevant excerpts when opening a ticket.


Reconciliation — verify your stored results​

After going live, periodically verify that the finStatus values your system has stored match the authoritative records at Handpoint. This is especially important after any incident (outage, crash, recovery).

Single transaction check​

# Verify the outcome stored for a specific transaction
curl -s "https://transactions.handpoint.com/transactions/{transactionReference}/status" \
-H "ApiKeyCloud: YOUR_MERCHANT_API_KEY" | jq '{finStatus, transactionID, totalAmount}'

Compare the returned finStatus to what your system has on record for the same transactionReference. A mismatch means your stored record is incorrect — investigate and correct it.

Batch reconciliation pattern (JavaScript)​

async function reconcile(storedTransactions, apiKey) {
const mismatches = [];

for (const txn of storedTransactions) {
const resp = await fetch(
`https://transactions.handpoint.com/transactions/${txn.transactionReference}/status`,
{ headers: { 'ApiKeyCloud': apiKey } }
);
const live = await resp.json();

if (live.finStatus !== txn.finStatus) {
mismatches.push({
transactionReference: txn.transactionReference,
stored: txn.finStatus,
live: live.finStatus,
liveTransactionID: live.transactionID
});
}
}

return mismatches;
}

// Usage
const issues = await reconcile(
[
{ transactionReference: 'uuid-1', finStatus: 'AUTHORISED' },
{ transactionReference: 'uuid-2', finStatus: 'DECLINED' }
],
process.env.HP_API_KEY
);

if (issues.length > 0) {
console.error('Reconciliation failures:', issues);
// Alert your operations team — each mismatch is a potential double-charge or missed payment
}

What to check​

MismatchLikely causeAction
Stored DECLINED, live AUTHORISEDRecovery flow did not save the resultCorrect the record; verify the cardholder was charged; check fulfilment
Stored AUTHORISED, live CANCELLEDPartial approval cancelled by cardholder; not reflected in your DBCorrect the record; verify no fulfilment happened; check for chargeback risk
Stored AUTHORISED, live UNDEFINED/status has no record (wrong reference, or gateway issue)Re-query after 24h; escalate to Handpoint Support with the reference

Per-operation validation scenarios​

Run the scenarios below for each operation your integration supports.

ViscusDummy trigger amounts

All staging / DEMO merchant testing uses ViscusDummy — a mock acquirer where the transaction outcome is determined by the amount sent (in minor units — cents/pence). Any card that can tap, insert, or swipe will work; card numbers are not validated.

AmountOutcomefinStatus
Any amount not listedApprovedAUTHORISED
3784Issuer decline (not authorized)DECLINED
3779Refer to card issuerDECLINED
3793Pick up cardDECLINED
3757Partial approval (totalAmount < requestedAmount)PARTIAL_APPROVAL
3768Request timeoutFAILED
3741Processing errorFAILED

→ Full list including SCA triggers: Development hardware — trigger amounts

Sale​

ScenarioTriggerExpected outcome
Standard approved saleAny non-trigger amount (e.g. 500)AUTHORISED; receipt URL or HTML present; transactionID stored
Cardholder cancels at terminalAny amount — press Cancel on terminalCANCELLED; no charge; transactionID is empty string
Issuer declines3784DECLINED; prompt for alternative card; transactionID is empty string
Gateway timeout / FAILED3768FAILED; log statusMessage; do not retry automatically
Partial approval3757PARTIAL_APPROVAL; display totalAmount; prompt split tender or reverse
Connection dropped mid-saleAny amount — drop network after 202Recovery flow resolves to definitive outcome (see above)
Page refresh mid-pollAny amount — refresh browser after 202recoverOnStartup() finds pending state; resumes polling; correct result stored

Verify for every approved sale:

  • finStatus === "AUTHORISED"
  • transactionID is a non-empty UUID
  • merchantReceipt / customerReceipt present (URL or HTML)
  • transactionReference in result matches what you sent
  • Your system persisted the result before clearing the UI

Refund​

ScenarioTriggerExpected outcome
Linked refund — same cardAny amount + same card as originalAUTHORISED; funds returned
Linked refund — card mismatchedAny amount + different cardDECLINED — document in your UX; instruct cardholder to use original card
Amount exceeds originalAmount > original totalAmountDECLINED — validate amount ISV-side before sending
After batch closeAny amount — attempt after batch closedDECLINED or gateway error; route to unlinked refund

Reversal​

ScenarioTriggerExpected outcome
Remote reversal (POST /reversal) — batch openoriginalGuid from same-day salehttpStatus: 200 (integer); issuerResponseCode: "00"; no finStatus
Remote reversal — batch closedSame as above after batch closeError 3153 — route to Refund
Double reversalSame originalGuid twiceError 3051 Already reversed
Wrong originalGuidRandom UUIDError 3153 or equivalent
On-terminal reversal — batch openoriginalTransactionId from same-day saleAUTHORISED polled result; no card required

Tip Adjustment​

ScenarioTriggerExpected outcome
Add tip on approved saletransactionID from a 3784-excluded sale; call tip-adjustment{"statusMessage": "tip adjusted"}
Amount 0 — void tipamount: 0 in bodyTip removed from transaction
Attempt after batch closeCall after POST /batch/closeRejected — issue a Refund for the tip amount instead
EPI: sale with on-screen tip already setPre-auth with tip; then tip-adjustRejected — document in your UX; prevent staff from attempting

Pre-Authorization​

Run the full pre-auth lifecycle — do not certify pre-auth without testing all sub-operations:

ScenarioTriggerExpected outcome
Create pre-authAny non-trigger amountAUTHORISED; hold placed; transactionID stored
Increase holdincreaseAmount in major unitsHold raised; verify in portal
Decrease holdincreaseAmount + subtract: "1"Hold reduced; verify in portal
Capture at original amountcapturedAmount = pre-auth amountSettled; hold cleared; preAuthorizationCaptureGuid returned
Capture at reduced amountcapturedAmount < pre-auth amountSettled at reduced amount
Void (release without capturing)preAuthorizationReversal operationHold released; no settlement
Attempt capture after voidoriginalGuid of voided pre-authError 3051 Already reversed (no-reader)
Attempt capture after captureoriginalGuid of already-captured pre-authError 3211 (no-reader) — already settled
Expired / unknown originalGuidRandom UUIDError 5001 (no-reader) — gateway NPE (known issue CUS-832)

Tokenization​

ScenarioExpected outcome
Tokenize-only (no charge)Token returned in result
Sale-and-tokenizeAUTHORISED + token returned
Token provider failure during sale-and-tokenizeEntire transaction declined — verify no partial charge
Deferred token retrieval from past GUIDToken returned via GET /transactions/{id}/token
Remote Sale using stored tokenAUTHORISED without any terminal interaction

Batch (EPI only)​

ScenarioExpected outcome
Manual batch closeBATCH_CLOSED; batch ID returned
Double closeBATCH_ALREADY_CLOSED
Batch summary after closeSummary returned with correct totals
Tip adjustment after manual closeRejected

Error codes to verify​

CodeTriggerCorrect ISV response
1001 Device is busySend two operations simultaneouslyQueue the second — do not retry immediately
3051 Already reversedDouble reversal or void-after-captureLog and suppress — no retry
3153 Unable to find message to reverseWrong originalGuid on POST /reversalVerify GUID; fall back to Refund
3211 Already settledCapture after captureCheck state via TXN Feed; issue Refund if needed
4066 Amount exceeds originalReversal/capture amount too highValidate amount before sending
5001 NullPointerExceptionUnknown originalGuid on preauth endpointsVerify GUID from pre-auth create result
VALIDATION_FAILED (422)Wrong field name (e.g. amount instead of increaseAmount)Check field names in request body
403 No valid key foundWrong or expired API keyVerify ApiKeyCloud header

See Error codes reference for the full list.

For merchant capability errors (pre-auth not enabled, MOTO not enabled, refund not allowed, partial reversal rejected) — including their exact error shapes, which tier enforces each one (gateway vs terminal), and the known ViscusDummy limitations — see Error Handling Guide — Merchant capability errors.


Staging vs production checklist​

Before switching to production credentials:

Core functionality

  • All required scenarios pass on staging with DEMO merchant
  • Transaction recovery tested and verified — page refresh / app crash mid-transaction resolves correctly
  • transactionReference persisted to durable storage before every API call
  • App timeout implemented — no silent abandonment on terminal hang
  • Partial approval handled (even if your UI shows "declined, try another card")
  • Acquirer-specific restrictions tested (e.g. EPI tip adjustment restrictions)

Receipt compliance

  • Receipts include all required EMV fields (AID, TVR, IAD, ARC when non-empty; authorisation code; retrieval reference; merchant name/address/MID/TID)
  • Receipt delivery tested for all your supported methods (email/SMS/print)
  • ISV-built receipt implemented for recovered transactions (no receipt URL on /status responses)
  • Receipt fields use totalAmount (not requestedAmount) as the displayed and settled amount

Merchant management

  • Per-merchant ApiKeyCloud stored in your backend against merchant records — not hardcoded, not shared between merchants
  • Staging and production base URLs and API keys switched together in your environment config

Logging

  • All API requests and responses logged with timestamps and transactionReference
  • Log retention confirmed at 14+ days
  • API key values not logged in plain text

Certification

  • Certification call completed with Handpoint Integration Support
  • Request/response log samples available for review
  • Production API key / SSK received and stored securely
  • Staging credentials removed or feature-flagged out of production builds