Card-present terminals · TMS · reporting

Handpoint API

Three Handpoint surfaces consolidated here: the REST API for taking payments on physical terminals (cloud.handpoint.com), the TMS API for provisioning merchants and terminals (tmsdoc.handpoint.com), and the Transaction Feed API for pulling settled transactions into your reporting (txnfeed.handpoint.io). Each surface has its own credential – they don’t cross over.

Reformatted from the public Handpoint docs. Where these docs and Handpoint’s upstream disagree, trust upstream.

Overview

Introduction

Handpoint is a P2PE-encrypted terminal cloud. Merchant software hits our REST API to tell a terminal to take a sale; the terminal runs the EMV / contactless / swipe flow and returns the result.

Three independent APIs ship under the Handpoint umbrella, each with its own auth and its own base URL:

  • REST API – taking payments on physical terminals.
  • TMS API – onboarding merchants, assigning terminals, minting API keys.
  • Transaction Feed API – pulling transactions for reporting / dashboards.
Building for a Merchant360 merchant? Use the Merchant360 API.

Merchant360 supports card-present and card-not-present payments in one API. Card-present sales go to the Handpoint reader through Merchant360’s REST API as a semi-integrated flow: your software sends the amount, the reader takes the card, and you get the result as the same payment object a keyed charge returns. You don’t need a Handpoint API key, host, device type or serial number. Card-present: https://www.cygma.cloud/merchant360#terminal. Every transaction for reporting, all tenders: https://www.cygma.cloud/merchant360#transactions. This page documents Handpoint’s native APIs for direct Handpoint integrations.

Cloud API · SDKs · .io vs .com

Integration paths and hosts

Two ways to drive a Handpoint terminal directly:

  • Cloud API (REST). Your server POSTs to a PAX terminal in Integrated (cloud) mode, then polls for the result or receives a callback. Auth: ApiKeyCloud header with a merchant Cloud key. A merchant can have several keys.
  • SDKs. Android SDK on PAX, Android / iOS for HiLite over Bluetooth, JavaScript SDK (through Cloud), Windows .NET, Cordova / Ionic. SDKs use the merchant’s SSK (one per merchant; GET /devices returns it).
PurposeProductionStaging (debug devices)Auth header
Cloud APIcloud.handpoint.comcloud.handpoint.ioApiKeyCloud: <key>
Status / recoverytransactions.handpoint.comtransactions.handpoint.ioApiKeyCloud: <key>
Transaction feedtxnfeed.handpoint.comtxnfeed.handpoint.ioAuthorization: <KEY>
TMStms.handpoint.comAuthorization: <PARTNER_KEY>
  • The .io hosts serve debug / staging devices and DEMO merchants only. Credentials do not work across .io and .com.
  • /status lives only on transactions.*; it returns 404 on cloud.*.
  • Amounts. Cloud and SDK operations take minor units as strings ("1000" = 10.00). Back-office endpoints (/reversal, /moto/*, /preauthorization/*, tip adjustment) take major units ("95.00").
  • Device names are serialNumber-terminalType.
One credential per surface

Authentication

Each surface uses a different credential and a different header:

  • REST API – ApiKeyCloud header. The key is unique per merchant; you mint it through the TMS API.
  • TMS API – JWT obtained from a Login endpoint, passed in Authorization: Bearer on subsequent calls.
  • Transaction Feed API – AnalyticsApiKey header. Separate key from the REST API.
↑ Request – what you send
# REST API (transactions on terminals)
ApiKeyCloud: <merchant-api-key>

# TMS API (merchant + terminal provisioning)
Authorization: Bearer <jwt-from-login>

# Transaction Feed API (reporting)
AnalyticsApiKey: <analytics-api-key>
Production vs debug

Environments

Pick the base URL by terminal type, not by where your code is running. Production / demo terminals hit cloud.handpoint.com; debug terminals hit cloud.handpoint.io. Demo terminals use a mock acquirer – no funds move, but every other behavior is real.

The .io hosts serve debug devices and DEMO merchants only. Credentials minted on one side do not work on the other. See Integration paths & hosts.

↑ Request – what you send
# Production (REST)        – also used for "demo" terminals (mock acquirer, no funds moved)
https://cloud.handpoint.com

# Debug / development (REST)
https://cloud.handpoint.io

# Transaction status / lookup (REST)
https://transactions.handpoint.com   # production
https://transactions.handpoint.io    # debug

# Transaction Feed
https://txnfeed.handpoint.com        # production
https://txnfeed.handpoint.io         # debug
Callback or polling

Flow models (push vs poll)

Every transaction request returns 202 Accepted with a transactionResultId – the terminal hasn’t actually run the transaction yet. You get the real result one of two ways:

  • Push (4.1) – supply callbackUrl + token in the request. Handpoint POSTs the result to your URL when the terminal finishes; the token echoes in AUTH-TOKEN so you can authenticate the callback. Your callback URL must accept a certificate authority Android 5-10 supports (terminal OS versions).
  • Poll (4.2) – omit those fields. GET /transaction-result/{id} with the returned id. Returns 204 while in flight, 200 with the result when done.
↑ Request – what you send
// Push (4.1) – include callbackUrl + token in the
// transaction request. Handpoint POSTs the result back to
// your callback when the terminal finishes; AUTH-TOKEN
// echoes the token you sent.

{
  "operation": "sale",
  "amount":    "10000",
  "currency":  "EUR",
  "callbackUrl": "https://your.app/handpoint-callback",
  "token":       "session-7c1d"
}

// Poll (4.2) — omit callbackUrl + token. The API returns
// transactionResultId immediately; you poll
// GET /transaction-result/{id} until you get a 200.

{
  "operation": "sale",
  "amount":    "10000",
  "currency":  "EUR"
}
callbackUrl + token · auth-token header

Callback URL

Send callbackUrl and token on the POST. When the terminal finishes, Handpoint POSTs the TransactionResult to your URL with the token in the auth-token header (it may arrive as AUTH-TOKEN; compare case-insensitively).

  • Use a fresh UUID as the token for every transaction and reject callbacks whose header does not match.
  • Retries: every 5 s for 100 s, then exponential backoff capped at 15 min, for up to 2 days. Any 2xx stops them.
  • recoveredTransaction: true marks a redelivery. Dedupe on transactionReference.
  • Return 200 even for UNDEFINED (keep resolving it through recovery).
  • The callback host’s TLS certificate must chain to a common CA (DigiCert, GlobalSign, Sectigo); terminal OS trust stores are limited.
Sale with a callback – POST /transactions
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/transactions \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "operation": "sale",
  "amount": "1800",
  "currency": "USD",
  "terminal_type": "PAXA920MAX",
  "serial_number": "2740199794",
  "transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f",
  "callbackUrl": "https://pos.example.com/handpoint/callback",
  "token": "6f2c0f4e-9a51-4c4b-8f0e-2d7b1f0c9e11"
}'
What Handpoint POSTs to your URL
↑ Request – what you send
POST /handpoint/callback HTTP/1.1
Host: pos.example.com
Content-Type: application/json
auth-token: 6f2c0f4e-9a51-4c4b-8f0e-2d7b1f0c9e11
01
API surface 1 of 3

REST API

Taking payments on physical terminals – sale, refund, void, pre-auth, MOTO, batch, and device controls.

cloud.handpoint.com·Auth: ApiKeyCloud header
Merchant360 merchants: send card-present sales through the Merchant360 API

POST /payments with terminal_id sends the sale to the merchant’s Handpoint reader (semi-integrated) with your Merchant360 key – the same endpoint, payment object and webhooks as card-not-present. Documentation: https://www.cygma.cloud/merchant360#terminal

GET /devices

List devices

Retrieves every payment terminal (and Virtual Terminal) associated with the merchant. Use the returned serial_number + terminal_type on every subsequent transaction request.

↑ Request – what you send
curl -X GET https://cloud.handpoint.com/devices \
  -H "ApiKeyCloud: <merchant-api-key>"
POST /transactions

Send transaction

Sends an operation to the terminal. Valid operation values include sale, refund, refundReversal, preAuthorizationCapture, and more.

amount is in minor units (cents) as a string: "10000" = €100.00. transactionReference must be a UUID v4 for original operations; linked operations (refund, reversal) pass originalTransactionId instead.

Common error codes: 1001 device busy, 1002 device offline / not responding, 1003 cancel operation not allowed.

Body parameters
FieldTypeDescription
operationrequiredstringTerminal operation: sale, refund, refundReversal, preAuthorizationCapture, etc.
amountrequiredstringAmount in minor units (cents) as a string. "10000" = €100.00.
currencyrequiredstringISO 4217 currency code, e.g. EUR.
terminal_typerequiredstringTerminal model from List devices, e.g. PAXA920.
serial_numberrequiredstringTerminal serial number from List devices.
transactionReferencerequiredstringUUID v4 for original operations; echoed back on the result.
customerReferenceoptionalstringYour own reference for the operation.
originalTransactionIdoptionalstringLinks a refund or reversal to the original transaction.
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/transactions \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "operation": "sale",
  "amount": "10000",
  "currency": "EUR",
  "terminal_type": "PAXA920",
  "serial_number": "1547854757",
  "customerReference": "op15248",
  "transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f"
}'
GET /transaction-result/{id}

Fetch result (poll)

Polling endpoint for flow model 4.2. Hit it until you get a 200. Returns the full transaction-result object with masked PAN, card scheme, AVS / CVV results, signature URL, customer + merchant receipt text, and the cardToken you can re-use for MOTO calls within 12 months.

Path parameters
FieldTypeDescription
idrequiredstringThe transactionResultId returned by POST /transactions.
↑ Request – what you send
curl -X GET https://cloud.handpoint.com/transaction-result/1850025030-1776238708721 \
  -H "ApiKeyCloud: <merchant-api-key>"
GET /transactions/{ref}/status

Transaction status

Query by the transactionReference you supplied on the original request. Useful when you’ve lost the transactionResultId or when you need to find every linked operation (refunds, reversals, captures) tied to one original sale – pass selector=all.

Lives on a different host: transactions.handpoint.com / .io.

finStatus values: IN_PROGRESS, UNDEFINED, PARTIAL_APPROVAL, AUTHORISED, DECLINED, CANCELLED, FAILED, PROCESSED, REFUNDED, CAPTURED. Which are final, timing, and retry rules: Transaction recovery.

FieldTypeDescription
transactionReferencerequiredstringPath – the reference you supplied on the original request.
selectoroptionalstringPath segment after /status/: first (default when omitted), last, all, or a numeric index n.
↑ Request – what you send
curl -X GET https://transactions.handpoint.com/transactions/{transactionReference}/status \
  -H "ApiKeyCloud: <merchant-api-key>"
finStatus · 360 s · never reuse a reference

Transaction recovery

Persist a UUID v4 transactionReference before you send, then resolve the outcome by result id (terminal channel) or by reference (status host). GET /transactions/{ref}/status returns the first operation for the reference; /status/{all|first|last|n} selects others. Each entry carries type, finStatus, totalAmount, transactionID, and transactionReference or originalEFTTransactionID. Net = AUTHORISED sales minus refunds and reversals; a later operation’s finStatus reflects its state when it was processed.

finStatusFinalMeaning / action
IN_PROGRESSNoKeep polling.
UNDEFINEDNoKeep polling until timeout. If the terminal never reached the gateway, the card was not charged.
PARTIAL_APPROVALNoUS: the terminal is prompting.
AUTHORISEDYesCharged. Do not retry.
DECLINEDYesNot charged. Retry allowed (new reference).
CANCELLEDYesNot charged. Retry allowed (new reference).
FAILEDYesNot charged. Retry allowed (new reference).
PROCESSEDYesTokenize or MOTO operation done.
REFUNDEDYes
CAPTUREDYes

Timing

  • Poll GET /transaction-result/{id} every 3 to 5 s (204 in progress, 200 result, 404 unknown). Card-present typically takes 10 to 30 s.
  • Worst case is about 340 s. Use an active-recovery timeout of 360 s from the send, then query /status once more. Still IN_PROGRESS or UNDEFINED: flag for manual reconciliation.
  • Never auto-retry with the same reference. A retry is a new operation with a new UUID, and only after a final non-charged status.

Two-thread variant

  • Thread 1 polls the terminal channel every 5 s (authoritative). Thread 2 polls /status every 10 s.
  • If /status says AUTHORISED and no terminal result has arrived, send pingDevice (expect PING_PONG).
  • After 300 s /status is provisional. If it says AUTHORISED and no terminal result ever came, reverse the sale.

Reference rules

  • Send transactionReference only on originals: sale, unlinked refund, saleAndTokenizeCard, preAuthorization. Not on reversal, linked refund, capture / void or tip adjust.
  • customerReference is an echo only. Duplicate protection is duplicate_check (default true).
  • KI-2026-008 (CUS-837): moToSale ignores transactionReference. Poll by transactionResultId and store transactionID.

Terminal-initiated reversals

The terminal can reverse an approved sale itself. The result comes back finStatus: CANCELLED with customFields.messageReasonCode 4000 plus a cause code in another custom field (Handpoint does not document that field’s name; match on the code values below). Do not retry and do not send a reversal.

CauseMeaning
4021Timeout after approval
4351Chip card removed early
4352Chip declined after host approval
4353Signature not provided in time
4354Signature declined or partial approval rejected
4580Processing error
4002Suspected terminal malfunction
Status by reference – first operation
↑ Request – what you send
curl -X GET https://transactions.handpoint.com/transactions/2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f/status \
  -H "ApiKeyCloud: <merchant-api-key>"
Every linked operation – /status/all
↑ Request – what you send
curl -X GET https://transactions.handpoint.com/transactions/2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f/status/all \
  -H "ApiKeyCloud: <merchant-api-key>"
Ping the device – POST /transactions
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/transactions \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "operation": "pingDevice",
  "terminal_type": "PAXA920MAX",
  "serial_number": "2740199794"
}'
Terminal-initiated reversal (result)
↑ Request – what you send
{
  "finStatus": "CANCELLED",
  "statusMessage": "Cancelled",
  "transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f",
  "customFields": {
    "messageReasonCode": "4000",
    "<cause field>": "4351"
  }
}
POST /transactions/{guid}/tip-adjustment · US only

Tip adjustment

Adjust the tip amount on an already-authorized sale before batch settlement. US restaurants only; TSYS / VANTIV acquirers only. amount is in major units (dollars) – note the mismatch with the transaction amount field which is in minor units.

EPI (TSYS): send it before the EPI batch auto-close (about 11pm EST); 0 voids the tip. Errors ORIGINAL_NOT_FOUND, BATCH_ALREADY_CLOSED, TIP_ADJUSTMENT_NOT_ENABLED, AMOUNT_EXCEEDS_LIMIT. See EPI acquirer features.

FieldTypeDescription
guidrequiredstringPath – GUID of the authorized sale to adjust.
amountrequiredstringBody – new tip amount in major units (dollars), e.g. "5.25".
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/transactions/ff6da784-8b57-11ed-9891-ebe2a88ff071/tip-adjustment \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "amount": "5.25"
}'
GET /transactions/{guid}/token

Card token (deferred tokenization)

Retrieve a card token from a completed card-present transaction so you can charge the same card via the MOTO endpoints later. Eligible transaction types: sale, refund, preAuthorizationCapture, moToSale, moToRefund. Must be requested within 12 months of the original transaction.

Path parameters
FieldTypeDescription
guidrequiredstringGUID of the completed transaction to tokenize.
↑ Request – what you send
curl -X GET https://cloud.handpoint.com/transactions/{guid}/token \
  -H "ApiKeyCloud: <merchant-api-key>"
POST /preauthorization/increase

Pre-auth increase / decrease

Modify the authorized amount on an open pre-authorization without re-running the card. increaseAmount is the delta in major units. Pass subtract: "1" to decrease.

Body parameters
FieldTypeDescription
originalGuidrequiredstringGUID of the open pre-authorization.
increaseAmountrequiredstringDelta to apply, in major units (dollars).
subtractoptionalstringSet to "1" to decrease instead of increase.
tipAmountoptionalstringOptional tip to add, in major units.
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/preauthorization/increase \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "originalGuid": "0c9d9df0-48ec-11eb-81a1-470a19c80d3a",
  "increaseAmount": "10.00"
}'
POST /preauthorization/capture

Pre-auth capture

Finalize an open pre-auth and actually charge the card. capturedAmount is in major units; can’t exceed the authorized amount (after any increases). Add a tip via tipAmount.

Body parameters
FieldTypeDescription
originalGuidrequiredstringGUID of the open pre-authorization.
capturedAmountrequiredstringAmount to capture, in major units; can't exceed the authorized amount.
tipAmountoptionalstringOptional tip to add, in major units.
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/preauthorization/capture \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "originalGuid": "0c9d9df0-48ec-11eb-81a1-470a19c80d3a",
  "capturedAmount": "120.00"
}'
POST /moto/sale

MOTO sale

Charge a previously-tokenized card without involving a terminal. cardToken comes from a prior card-present sale (or the deferred-tokenization endpoint). amount in major units.

Body parameters
FieldTypeDescription
amountrequiredstringAmount to charge, in major units (dollars).
currencyrequiredstringISO 4217 currency code.
cardTokenrequiredstringToken from a prior card-present sale or the deferred-tokenization endpoint.
transactionReferencerequiredstringUUID v4 for this operation.
customerReferenceoptionalstringYour own reference for the order.
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/moto/sale \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "amount": "20.00",
  "currency": "EUR",
  "cardToken": "665630867",
  "customerReference": "order-12345",
  "transactionReference": "b7b2360d-3e9e-4b62-9a3a-2e6ef6c5cd01"
}'
POST /transactions · operation moToSale

Key entry sale (moToSale)

moToSale without a cardToken shows the terminal’s manual entry screen. Optional channel MO or TO. AVS: send enableAvsFields: true (the terminal asks) or a billing object (zipCode required, address optional). Needs the merchant flags motoEnabled (and avsForMoto for AVS) in TMS; otherwise MOTO_NOT_ENABLED. moToRefund is the keyed refund.

Handpoint has no per-sale switch that shows or hides a keyed option on a normal sale; keyed entry is its own operation. KI-2026-008: poll moToSale by transactionResultId.

Open manual entry on the terminal
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/transactions \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "operation": "moToSale",
  "amount": "1800",
  "currency": "USD",
  "terminal_type": "PAXA920MAX",
  "serial_number": "2740199794",
  "channel": "MO",
  "enableAvsFields": true
}'
With billing for AVS
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/transactions \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "operation": "moToSale",
  "amount": "1800",
  "currency": "USD",
  "terminal_type": "PAXA920MAX",
  "serial_number": "2740199794",
  "channel": "TO",
  "billing": {
    "zipCode": "11933",
    "address": "1161 SCOTT AVE"
  }
}'
Error codes
↑ Request – what you send
# moToSale
MOTO_NOT_ENABLED   merchant motoEnabled (or avsForMoto for AVS) is off in TMS
POST /moto/refund

MOTO refund

Refund a prior MOTO sale (linked by originalGuid). Amount can’t exceed the original; currency must match.

Body parameters
FieldTypeDescription
originalGuidrequiredstringGUID of the MOTO sale being refunded.
amountrequiredstringRefund amount in major units; can't exceed the original.
currencyrequiredstringMust match the original transaction's currency.
transactionReferencerequiredstringUUID v4 for this operation.
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/moto/refund \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67",
  "amount": "5.00",
  "currency": "EUR",
  "transactionReference": "a1fe8db5-69a4-4b4d-a704-94ac2570f9b0"
}'
POST /moto/reversal

MOTO reversal

Void a previous MOTO sale within the reversal window. Deprecated by Handpoint: use POST /reversal.

Body parameters
FieldTypeDescription
originalGuidrequiredstringGUID of the MOTO sale to void.
amountrequiredstringAmount to reverse, in major units.
currencyrequiredstringISO 4217 currency code.
transactionReferencerequiredstringUUID v4 for this operation.
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/moto/reversal \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "originalGuid": "b28bdb10-cf87-11f0-b588-a122fae316de",
  "amount": "20.00",
  "currency": "EUR",
  "transactionReference": "4d7b1a2c-5bfd-4a30-9b6f-123456789abc"
}'
POST /reversal · synchronous · major units

Remote reversal

Reverse a sale without the terminal. originalGuid is the sale’s transactionID; amount (major units) is optional and makes it a partial reversal where the acquirer supports it. messageReasonCode: CUSTOMER_CANCELLATION (default) or TIMEOUT_WAITING_FOR_RESPONSE. The call is synchronous. /moto/reversal is deprecated; use /reversal.

FieldTypeDescription
originalGuidrequiredstringtransactionID of the sale.
amountoptionalstringMajor units. Omit for the full amount.
currencyrequiredstringISO 4217.
messageReasonCodeoptionalstringCUSTOMER_CANCELLATION (default) | TIMEOUT_WAITING_FOR_RESPONSE.

Errors: 3158 not found, 3051 already reversed, 4066 partial amount exceeds the original. On-device reversal is saleReversal with originalTransactionId, same business day.

Full reversal
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/reversal \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67",
  "currency": "USD",
  "messageReasonCode": "CUSTOMER_CANCELLATION"
}'
Partial reversal (EPI)
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/reversal \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67",
  "amount": "5.00",
  "currency": "USD",
  "messageReasonCode": "CUSTOMER_CANCELLATION"
}'
Error codes
↑ Request – what you send
# POST /reversal
3158   original transaction not found
3051   already reversed
4066   partial amount exceeds the original
Beta · acquirer-dependent

Batch close / summary / detail

Three related endpoints, all POST, all take { deviceType, serialNumber, batchNumber } in the body:

POST/batch/closeRequest batch closure.
POST/batch/summaryTotals + transaction count (same body).
POST/batch/detailArray of every transaction in the batch (filter with optional rrn).

All three are Beta and depend on the underlying acquirer supporting them.

Body parameters
FieldTypeDescription
deviceTyperequiredstringTerminal model, e.g. PAXA920MAX.
serialNumberrequiredstringTerminal serial number.
batchNumberrequiredstringBatch to act on.
rrnoptionalstring/batch/detail only: filter to a single transaction by RRN.
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/batch/close \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "deviceType": "PAXA920MAX",
  "serialNumber": "2740013262",
  "batchNumber": "1"
}'
POST /devices/{type}/{serial}/{command}

Device commands

Administrative commands for a terminal. Every endpoint returns 202 Accepted when the command is queued. Requires the Handpoint Payments App 4.6.0+ in Integrated mode (or SDK 7.1006.0+). Common error: 400 DeviceNotListening means the terminal isn’t connected right now.

POST/devices/{type}/{serial}/set-security-policyBody { hideTabBar, blockSystemUI, brightness, passwordToExit }. Needs Payments App 4.14.1 / SDK 7.1014.1.
POST/devices/{type}/{serial}/set-unattended-modeKiosk. Body { status: true|false }. Persists across reboot since 4.11.0; locks on power disconnect since 4.14.1.
POST/devices/{type}/{serial}/set-localeBody { locale: "en_US" }.
POST/devices/{type}/{serial}/set-password-protectedBody { status: true|false }.
POST/devices/{type}/{serial}/rebootBody { force: false }; true reboots mid-transaction.
POST/devices/{type}/{serial}/set-screen-brightnessBody { minimumBrightnessLevel, maximumBrightnessLevel } (0 to 100).
POST/devices/{type}/{serial}/set-reboot-timeBody { hour: 22 } (0 to 23). Production only.

Other POS levers on POST /transactions: stopCurrentTransaction, pingDevice, printReceipt, bypassOptions, tipConfiguration.headerName / footer, a preset tipAmount, and moToSale without a card token (keyed screen). Not available from Handpoint: idle-screen branding or logos, hiding the cancel key, a per-sale keyed option on a normal sale, and pushing customer-display messages.

FieldTypeDescription
deviceTyperequiredstringPath – terminal model.
serialNumberrequiredstringPath – terminal serial number.
statusoptionalbooleanBody – set-unattended-mode / set-password-protected: enable or disable.
hideTabBar / blockSystemUIoptionalbooleanBody – set-security-policy: hide the app’s tab bar; block the Android system UI.
brightnessoptionalintegerBody – set-security-policy: screen brightness, 0-100.
passwordToExitoptionalstringBody – set-security-policy: password required to leave the app.
localeoptionalstringBody – set-locale: IETF BCP 47 tag, e.g. en_CA.
forceoptionalbooleanBody – reboot: true reboots mid-transaction.
minimumBrightnessLeveloptionalintegerBody – set-screen-brightness: low end of range, 0-100.
maximumBrightnessLeveloptionalintegerBody – set-screen-brightness: high end of range, 0-100.
houroptionalintegerBody – set-reboot-time: daily auto-reboot hour, 0-23 (production only).
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/devices/{deviceType}/{serialNumber}/set-unattended-mode \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "status": true
}'
/acquirers/epi · capability matrix

EPI acquirer features

What Handpoint supports when EPI is the acquirer (TSYS). Merchant flags (MOTO, AVS for MOTO, partial reversal, tip adjust, batch) are server-side TMS settings.

OperationCloudPAXHiLiteBack office
SaleYesYesYes
RefundYesYesYes
ReversalYesYesYesYes
Tip adjustmentYesYesYesYes
Pre-authYesYesYes
Pre-auth capture reversalYesYesYes
MOTOYesYes
TokenizationYesYesYes
BatchYes (US/CA, TMS-enabled)

Sale fields (Cloud)

amount, currency, terminal_type, serial_number, customerReference, transactionReference, callbackUrl, token, taxInformation, purchaseOrderNumber, duplicate_check, bypassOptions {signatureBypass, pinBypass}, tipConfiguration, merchantAuth, metadata, moneyRemittanceOptions, billing.

Features
FieldTypeDescription
Partial reversaloptionalEPI onlyPer-merchant TMS enablement. Non-cumulative. Not on Amex. Cloud, PAX, Cordova, Windows, REST. Errors AMOUNT_EXCEEDS_ORIGINAL, OPERATION_NOT_SUPPORTED, ORIGINAL_NOT_FOUND, 403, 400 “Device is busy”.
TokenizationoptionalEPI / PROCHARGEtokenizeCard (no amount, result PROCESSED) and saleAndTokenizeCard (no tipConfiguration). Deferred: GET /transactions/{transactionID}/token (Cloud 2.26.0). Result cardToken, cardTokenProvider (EPI | PROCHARGE), expiryDateMMYY. Errors TOKENIZATION_NOT_ENABLED, CARD_READ_ERROR.
Remote saleoptionalMAJOR unitsPOST /moto/sale {amount, currency, cardToken, transactionReference?, customerReference?, channel?, billing?, taxInformation?, purchaseOrderNumber?}. Errors INVALID_TOKEN, MOTO_NOT_ENABLED. TMS-enabled.
Remote refundoptionalMAJOR unitsPOST /moto/refund {originalGuid, amount} within 12 months. KI-2024-005: on sales older than 90 days it returns SUCCESS but is not applied on TSYS.
EMV refundoptionalterminalLinked by originalTransactionId (same card enforced) or unlinked by transactionReference. A wallet tap and the physical card produce different PAN tokens. Errors ORIGINAL_NOT_FOUND, UNLINKED_REFUND_NOT_ALLOWED.
Remote reversaloptionalMAJOR unitsSee Remote reversal.
TipoptionalterminaltipConfiguration {tipPercentages (required), baseAmount, headerName, enterAmountEnabled, skipEnabled, footer}, or a preset tipAmount that skips the tip screen. The result carries tipAmount and totalAmount.
Tip adjustmentoptionalMAJOR unitsPOST /transactions/{transactionID}/tip-adjustment {amount}; 0 voids the tip. Before the EPI batch auto-close (about 11pm EST). To combine with a partial refund: zero the tip, refund, re-adjust. Errors ORIGINAL_NOT_FOUND, BATCH_ALREADY_CLOSED, TIP_ADJUSTMENT_NOT_ENABLED, AMOUNT_EXCEEDS_LIMIT.
Pre-authoptionalterminal + back officeCreate with preAuthorization / moToPreAuthorization. Increase: POST /preauthorization/increase {originalGuid, increaseAmount, subtract "0"|"1", customerReference} returns cumulative holdAmount, increaseAmount, originalAmount. Capture: POST /preauthorization/capture {originalGuid, capturedAmount, tipAmount?, customerReference?, taxInformation?, purchaseOrderNumber? (max 25)}, not above the hold. Void / capture reversal: preAuthorizationReversal or /reversal, full amount, before batch close. Holds expire in 7 to 30 days. Terminal-less increase / capture since Cloud 2.27.0.
Batch (beta)optionalCloud 2.25.0POST /batch/close {serialNumber, deviceType, batchNumber}; /batch/summary returns transactionCount, netAmount, customFields.salesCount / refundsCount; /batch/detail returns 5 per page, cursor = last retrievalReferenceNumber.
Level 2optionalCloud 2.34.0taxInformation {taxAmount, taxExempt} + purchaseOrderNumber. Violations return 400.
Multi-MIDoptionalCloud, Android PAXmerchantAuth: [{ externalId }], max 23 characters, must match a provisioned subMerchantExternalId. A bad id declines with “Invalid Merchant”, arc 1000. Not echoed in the result.
Feeoptionalunder developmentAndroid PAX SDK fee {amount, mitigationProgram, taxOnFee} only. Cloud does not take fee yet; the flat surchargeAmount still works.
Sale with tip, Level 2 and multi-MID
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/transactions \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "operation": "sale",
  "amount": "10000",
  "currency": "USD",
  "terminal_type": "PAXA920MAX",
  "serial_number": "2740199794",
  "transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f",
  "customerReference": "order-1042",
  "duplicate_check": true,
  "tipConfiguration": {
    "tipPercentages": [
      15,
      18,
      20
    ],
    "headerName": "Add a tip",
    "enterAmountEnabled": true,
    "skipEnabled": true,
    "footer": "Thank you"
  },
  "taxInformation": {
    "taxAmount": "8.25",
    "taxExempt": false
  },
  "purchaseOrderNumber": "PO-55120",
  "merchantAuth": [
    {
      "externalId": "EPI-SUB-0001"
    }
  ],
  "bypassOptions": {
    "signatureBypass": true,
    "pinBypass": false
  }
}'
Sale and tokenize
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/transactions \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "operation": "saleAndTokenizeCard",
  "amount": "2500",
  "currency": "USD",
  "terminal_type": "PAXA920MAX",
  "serial_number": "2740199794",
  "transactionReference": "7f3a1c22-5b6e-4d0a-9c8b-1e2f3a4b5c6d"
}'
Remote sale on a token – POST /moto/sale
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/moto/sale \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "amount": "25.00",
  "currency": "USD",
  "cardToken": "665630867",
  "transactionReference": "b7b2360d-3e9e-4b62-9a3a-2e6ef6c5cd01",
  "customerReference": "order-1043",
  "channel": "MO",
  "taxInformation": {
    "taxAmount": "2.06",
    "taxExempt": false
  },
  "purchaseOrderNumber": "PO-55121"
}'
Remote refund – POST /moto/refund
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/moto/refund \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67",
  "amount": "5.00"
}'
Tip adjust – POST /transactions/{transactionID}/tip-adjustment
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/transactions/1a41d9f0-cf72-11f0-95b2-770b7d1d8e67/tip-adjustment \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "amount": "3.00"
}'
Pre-auth increase – POST /preauthorization/increase
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/preauthorization/increase \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67",
  "increaseAmount": "20.00",
  "subtract": "0",
  "customerReference": "tab-14"
}'
Pre-auth capture – POST /preauthorization/capture
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/preauthorization/capture \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67",
  "capturedAmount": "110.00",
  "tipAmount": "10.00",
  "customerReference": "tab-14",
  "taxInformation": {
    "taxAmount": "9.08",
    "taxExempt": false
  },
  "purchaseOrderNumber": "PO-55122"
}'
Batch summary – POST /batch/summary (beta)
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/batch/summary \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "serialNumber": "2740199794",
  "deviceType": "PAXA920MAX",
  "batchNumber": "112"
}'
Interac VOID · PAYSAFE / TOKENEX tokens

Paysafe (Handpoint direct)

For merchants whose acquirer on Handpoint is Paysafe. Paysafe merchants integrate with Handpoint directly; they are not on Merchant360.

OperationSurfaces
Sale, sale with tip, refund, reversal, tip adjustment, tokenizationCloud, PAX, HiLite
VoidInterac only
MOTO / key entryCloud, PAX, JavaScript, Windows
Remote refund/moto/refund, within 12 months
Not supportedPartial reversal, pre-auth (listed as coming soon), remote sale, batch

Tokens

  • PAYSAFE: tokenizeCard / saleAndTokenizeCard return cardTokenProvider: "PAYSAFE", a single-use token that lives about 15 minutes, for Paysafe’s Customer Vault. It does not work with Handpoint MOTO. Needs Paysafe onboarding.
  • TOKENEX: cardTokenProvider: "TOKENEX", for loyalty and card matching only. No detokenize, no MOTO.

Interac

  • Interac routes to TNS; other cards to TSYS. Batch close is TSYS only.
  • Interac supports sale, sale with tip (the tip is folded into the total) and VOID only.
  • VOID = Cloud refund with originalTransactionId, currency: "CAD", no transactionReference. Card present, same day, full amount. Label the button “VOID” in your UI.
  • No remote reversal or refund, pre-auth, tip adjust or keyed entry for Interac.
  • Errors: CARD_NOT_PRESENT, ALREADY_SETTLED, ORIGINAL_NOT_FOUND.

Refunds in the Paysafe Portal are card-not-present and outside Handpoint; after 24 h they use the eftTransactionID as MerchantRefNum.

Interac VOID – POST /transactions (refund, linked, CAD)
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/transactions \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "operation": "refund",
  "amount": "2500",
  "currency": "CAD",
  "terminal_type": "PAXA920PRO",
  "serial_number": "1850012345",
  "originalTransactionId": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67"
}'
Tokenize (PAYSAFE single-use token)
↑ Request – what you send
curl -X POST https://cloud.handpoint.com/transactions \
  -H "ApiKeyCloud: <merchant-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
  "operation": "tokenizeCard",
  "terminal_type": "PAXA920PRO",
  "serial_number": "1850012345"
}'
Interac error codes
↑ Request – what you send
# Interac VOID
CARD_NOT_PRESENT     the card must be presented on the terminal
ALREADY_SETTLED      VOID is same day only
ORIGINAL_NOT_FOUND   originalTransactionId not found
02
API surface 2 of 3

TMS API

Terminal Management System – create merchants, assign terminals, mint API keys for the REST surface. Provisioning only; no payments cross this API.

tmsdoc.handpoint.com·Auth: Bearer JWT from POST /login
JWT auth

Login

Login returns a JSON Web Token. Send it in the Authorization: Bearer … header on every other TMS call. Tokens have a fixed lifetime – handle re-login when calls return 401.

↑ Request – what you send
# 1. Login (credentials → JWT)
POST /login

# 2. Every subsequent call:
Authorization: Bearer <jwt>
Create · read · update · delete

Merchants

Merchant lifecycle. The POST /merchants body must match your partner configuration (ISV id, acquirer id, etc.) – ask your Handpoint contact for the exact shape.

GET /merchants/{id} returns the merchant’s shared secret and other auth-grade fields; treat the response like a credential. After PUT, re-publish each assigned terminal’s configuration (see Lifecycle below).

↑ Request – what you send
POST   /merchants               create
GET    /merchants               list all under the partner
GET    /merchants/{id}          retrieve profile (includes shared secret)
PUT    /merchants/{id}          update – re-publish terminals after
DELETE /merchants/{id}          must have zero assigned terminals
Create · assign · publish · unassign · delete

Terminals

Each terminal lives in one of two states: unassigned (in the pool, no merchant) or assigned (attached to exactly one merchant). Reassignment is unassign → assign.

You must call POST /publish after every assign / unassign / merchant-edit. Without it the Handpoint Gateway doesn’t know about the change and the terminal will keep authorizing under the old merchant context.

↑ Request – what you send
POST   /terminals                          add to unassigned pool
GET    /terminals/unassigned               list pool (searchable)
POST   /terminals/{id}/assign              attach to a merchant
POST   /terminals/{id}/publish             push config to Gateway
                                             (REQUIRED after assign / change)
POST   /terminals/{id}/unassign            back to the pool
GET    /merchants/{m}/terminals            list assigned (searchable)
DELETE /terminals/{id}                     terminal must be unassigned first
Mint REST API + Txn Feed keys

API keys

Cloud API keys are minted per merchant from the TMS. The POST response includes the unmasked key – that’s the only time you’ll see it. Store it server-side before handing to the merchant.

↑ Request – what you send
POST   /merchants/{id}/api-keys           mint a new Cloud API key
GET    /merchants/{id}/api-keys           list (masked)
DELETE /merchants/{id}/api-keys/{key}     revoke
Standard onboarding

Lifecycle workflow

Standard onboarding for a new merchant + first terminal is six API calls in sequence:

  • Login → JWT.
  • Create the merchant.
  • Add the terminal to the unassigned pool.
  • Assign the terminal to the merchant.
  • Publish the terminal config to the Gateway.
  • Mint the merchant’s REST API key.
↑ Request – what you send
# 1. Login                                 → JWT
# 2. POST   /merchants                     → create
# 3. POST   /terminals                     → add terminal to pool
# 4. POST   /terminals/{id}/assign         → attach to merchant
# 5. POST   /terminals/{id}/publish        → tell the Gateway
# 6. POST   /merchants/{id}/api-keys       → mint REST key for the merchant

# Ongoing – after ANY merchant edit or terminal assignment change:
#   re-publish each affected terminal.
03
API surface 3 of 3

Transaction Feed API

Read-only reporting side – pull transactions, run aggregates, build dashboards. Separate analytics key from the REST API; cannot take payments. API version 2.8.0.

txnfeed.handpoint.io·Auth: AnalyticsApiKey header
Merchant360 merchants: one feed for every tender

GET /transactions on the Merchant360 API returns the merchant’s Handpoint reader sales together with card, ACH, cash, check, gift card and eGift activity, in one shape, with a Merchant360 key. Documentation: https://www.cygma.cloud/merchant360#transactions

Header-based

Authentication

Different credential from the REST API. The Transaction Feed is a read-only reporting surface; the key here doesn’t let you take payments. Ask Handpoint to provision a separate Analytics key – don’t reuse the Cloud API key.

↑ Request – what you send
curl -X POST https://txnfeed.handpoint.io/transactions \
  -H "AnalyticsApiKey: Your-Api-Key" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
  "startDate": "20260101000000",
  "endDate": "20260201000000"
}'
Date-range filter

Request config

Date format is a single string of digits: YYYYMMDDhhmmss. 20260609143200 = June 9, 2026 at 14:32:00. Both startDate and endDate are required on every POST.

Body parameters
FieldTypeDescription
startDaterequiredstringWindow start, YYYYMMDDhhmmss (14 digits, 24-hour, no separators).
endDaterequiredstringWindow end, same format.
↑ Request – what you send
// Every POST takes a RequestConfig body
{
  "startDate": "20260101000000",
  "endDate":   "20260201000000"
}

// Format: YYYYMMDDhhmmss (14 chars, 24-hour, no separators).
Five flavors

Transaction lookups

Five basic lookups, two type-filters (payouts = income, refundsandreversals = outflow), plus admin variants scoped to a specific merchant or to the partner level.

Every endpoint is POST so the body can carry the date range. The path parameters (guid, customerReference, partnerIdAlpha, merchantIdAlpha) sit in the URL.

FieldTypeDescription
guidoptionalstringPath – single-transaction endpoints: the transaction GUID.
customerReferenceoptionalstringPath – by-reference endpoints: your own reference.
partnerIdAlphaoptionalstringPath – admin endpoints: partner scope.
merchantIdAlphaoptionalstringPath – admin endpoints: merchant scope.
guidsoptionalarrayBody – /transactions/guids: bulk list of GUIDs.
customerReferencesoptionalarrayBody – /transactions/customerreferences: bulk list of references.
↑ Request – what you send
POST /transactions                                                # all transactions in window
POST /transactions/{guid}                                         # one by guid
POST /transactions/guids                                          # bulk by guid (body)
POST /transactions/customerreference/{customerReference}          # by your reference
POST /transactions/customerreferences                             # bulk by your reference

# Pre-filtered by direction:
POST /transactions/payouts                                        # sales + captures
POST /transactions/refundsandreversals                            # refunds + reversals

# Admin endpoints (require partner + merchant ids):
POST /transactions/{partnerIdAlpha}/{merchantIdAlpha}
POST /transactions/{partnerIdAlpha}/{merchantIdAlpha}/payouts
POST /transactions/{partnerIdAlpha}/{merchantIdAlpha}/refundsandreversals

# Partner-or-merchant context:
POST /transactions/partnerormerchant
POST /transactions/partnerormerchant/payouts
POST /transactions/partnerormerchant/refundsandreversals
Avg · Sum · Count · Distinct count · Net

Aggregations

Five aggregation families. {field} is the JSON attribute you’re aggregating over (amount, serialNumber, cardType, etc.). Net is a built-in convenience: payouts minus refunds+reversals.

Path parameters
FieldTypeDescription
fieldrequiredstringJSON attribute to aggregate over, e.g. amount, serialNumber, cardType. (Count endpoints omit it.)
↑ Request – what you send
# Avg
POST /transactions/aggregate/avg/{field}
POST /transactions/aggregate/avg/payouts/{field}
POST /transactions/aggregate/avg/refundsandreversals/{field}

# Sum
POST /transactions/aggregate/sum/{field}
POST /transactions/aggregate/sum/payouts/{field}
POST /transactions/aggregate/sum/refundsandreversals/{field}

# Count
POST /transactions/count
POST /transactions/count/payouts
POST /transactions/count/refundsandreversals

# Distinct count
POST /transactions/aggregate/distinctcount/{field}

# Net (payouts − refundsandreversals)
POST /transactions/aggregate/net/{field}
groupBy + calendarInterval

Grouping + time series

Add /group/ to the path and supply three query params:

FieldTypeDescription
oprequiredstringPath – aggregation op: avg, sum, count, distinctcount, or net.
fieldrequiredstringPath – JSON attribute to aggregate over (omit for count).
groupByrequiredstringQuery – comma-separated field names. Multiple keys = multiple group dimensions.
calendarIntervaloptionalstringQuery – time bucket size, e.g. HOUR, DAY, MONTH.
orderoptionalstringQuery – ASC or DESC.
↑ Request – what you send
# Generic grouped aggregate (works for avg/sum/count/distinctcount/net)
POST /transactions/aggregate/group/{op}/{field}?groupBy=serialNumber,cardType&calendarInterval=DAY&order=DESC

# Variants:
/aggregate/group/avg/{field}
/aggregate/group/avg/payouts/{field}
/aggregate/group/avg/refundsandreversals/{field}
/aggregate/group/sum/{field}
/aggregate/group/count
/aggregate/group/distinctcount/{field}
/aggregate/group/net/{field}
Visualization-ready

Graph endpoints

Same shape as /group/ but the response is pre-bucketed for charting libraries (timestamps on the X axis, the aggregate on Y). Skip the per-row mapping you’d normally do for a graph.

FieldTypeDescription
fieldrequiredstringPath – JSON attribute to aggregate over (omit for count).
groupByrequiredstringQuery – comma-separated field names.
calendarIntervaloptionalstringQuery – time bucket size, e.g. DAY.
orderoptionalstringQuery – ASC or DESC.
↑ Request – what you send
POST /transactions/aggregate/group/graph/avg/{field}
POST /transactions/aggregate/group/graph/sum/{field}
POST /transactions/aggregate/group/graph/count
POST /transactions/aggregate/group/graph/distinctcount/{field}
POST /transactions/aggregate/group/graph/net/{field}

# Same groupBy / calendarInterval / order params as the
# non-graph variants. Response shape is pre-bucketed for
# direct charting.
Common status codes

Errors

StatusMeaning
200OK
202Accepted – operation queued, result via callback or polling.
204No content – used by the polling result endpoint while in-flight.
400Bad request. REST error codes: 1001 busy, 1002 offline, 1003 cancel not allowed, 3107 CVV required, 3209 amount exceeds original, 3210 currency mismatch, 3211 pre-auth closed, 5252 token failure.
403Authentication failed or insufficient permissions.
404Resource not found (transactionResultId, transactionReference, etc.).
422Unprocessable – missing required fields.
5xxInternal Handpoint error.