Skip to main content

EPI

Sale​

EMV Sale​

On-device · chip, contactless, or magstripeCloud APIAndroid (PAX)Android (HiLite)iOS (HiLite)CordovaJavaScript SDKWindows (.NET)

Standard chip or contactless card-present payment — the cardholder taps, inserts, or swipes their card at the terminal. The terminal handles card entry mode automatically.

When to use it​

Use for standard retail and hospitality transactions where the cardholder is physically present and the amount is fixed before checkout. For a final amount that may change after authorisation (e.g. restaurant tab), use Pre-Authorization instead.

Code​

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "sale",
"amount": "1000",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"customerReference": "order-5248",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f"
}

Amount is in the smallest currency unit — "1000" = $10.00 USD. The 202 response returns a transactionResultId — poll GET /transaction-result/{transactionResultId} on cloud.handpoint.com for the outcome. Your transactionReference UUID v4 can also be used to query the full operation chain via GET /transactions/{transactionReference}/status/all on transactions.handpoint.com.

Parameters​

NameTypeRequiredDescription
operationstringYes (Cloud API)"sale"
amountstring / BigIntegerYesSmallest currency unit — "1000" = $10.00
currencystringYesISO 4217 code, e.g. "USD", "EUR", "CAD"
serial_numberstringYes (Cloud API)Target terminal serial number
terminal_typestringYes (Cloud API)Terminal model, e.g. "PAXA920"
transactionReferencestringNo (Cloud API / JS SDK)UUID v4 — send on original transactions only; omit on reversals and linked refunds
customerReferencestringNoMerchant reference forwarded to the acquirer
callbackUrlstringNo (Cloud API)URL to receive the result via POST; if omitted, poll GET /transaction-result/{id}

Android SDK SaleOptions fields (optional third argument to hapi.sale()):

FieldTypeDescription
customerReferenceString?Merchant reference echoed in TransactionResult
tipConfigurationTipConfiguration?Pre-configure tip prompt on the terminal
pinBypassBooleanOffer PIN bypass where acquirer-supported
checkDuplicatesBooleanEnable duplicate detection on the gateway
merchantAuthMerchantAuth?Override MID/TID for multi-MID merchants

Errors​

CodeMeaningRecovery
DECLINEDIssuer declinedAsk cardholder to try another card
CANCELLEDCardholder cancelled at terminalNo action required
TIMEOUTTerminal did not respondCheck connection; retry
COMMUNICATION_ERRORNetwork failureVerify connectivity; retry
PARTIAL_APPROVALIssuer approved a lesser amountAccept partial amount or reverse; see Partial Approval

Edge cases​

ScenarioBehaviour
Contactless limit exceededTerminal falls back to chip insert — instruct the cardholder to insert their card
Card chip read failureTerminal retries up to 3 times, then offers magstripe fallback — acquirer support for swipe varies
Duplicate detectionIf checkDuplicates is enabled and the same card + amount is seen within the window, the gateway rejects the second transaction
Connection drops mid-salePersist transactionReference before awaiting the result — query GET /transactions/{transactionReference}/status/all to recover the outcome

Testing​

Test on the TEST/DEMO merchant or staging device. Use test cards provided by your acquirer for specific scenarios.

ScenarioHow to trigger
ApprovedSend a valid request — verify finStatus: AUTHORISED, note transactionID for reversal tests
DeclinedUse acquirer test card for decline — verify finStatus: DECLINED
Partial approvalUse acquirer test card for partial approval — verify finStatus: PARTIAL_APPROVAL and that your app handles it
Timeout recoveryDrop connectivity mid-transaction — query GET /transactions/{transactionReference}/status/all to confirm outcome

Key Entry Sale​

On-device · operator keys card numberCloud APIAndroid (PAX)JavaScript SDKWindows (.NET)

Commands a PAX terminal to display a manual card entry screen — the cashier types the cardholder's card number, expiry, and CVV directly on the terminal's touchscreen. The terminal tokenizes the entry internally and processes it as a MOTO transaction. The ISV system never handles raw card data.

Processed as MOTO — acquirer fees apply

Despite using a physical PAX terminal, Key Entry Sale is submitted to the acquirer as a MOTO (card-not-present) transaction, not as a card-present key-entry transaction. This is because the card is manually entered rather than electronically read. MOTO transactions typically carry higher interchange rates than card-present transactions. Confirm the fee structure with your acquirer during merchant onboarding.

When to use it​

Use for phone orders where the cardholder reads their card details aloud to a call centre agent, or in-person situations where the card cannot be read electronically. The PAX terminal must be present in your environment and running in integrated mode.

Card data stays on the terminal

Unlike back-office MOTO (which uses a stored card token), key entry sale requires the PAX terminal to be physically present. The operator's system sends the command; the terminal's screen collects the card details.

Code​

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "moToSale",
"amount": "1000",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f"
}

Amount in smallest currency unit. The terminal shows a card entry screen — the operator types card details on device.

Address Verification (AVS)​

EPI supports optional Address Verification on Key Entry Sale. The terminal can prompt for the cardholder's billing postal code after card entry, or you can pass it programmatically.

// Option A — terminal prompts for zip code after card details are entered
val options = MoToOptions(enableAvsFields = true)
hapi.motoSale(BigInteger("1000"), Currency.USD, options)

// Option B — supply billing data from your own UI
val billing = Billing(zipCode = "10001", address = "123 Main St")
val options = MoToOptions(billing = billing)
hapi.motoSale(BigInteger("1000"), Currency.USD, options)

Do not combine enableAvsFields with billing — they are mutually exclusive. Requires avsForMoto and motoEnabled = true (both set per merchant by Handpoint).

info

See AVS for prerequisites and edge cases.

Parameters​

NameTypeRequiredDescription
operationstringYes (Cloud API)"moToSale" — triggers card entry screen on terminal
amountstringYesSmallest currency unit — "1000" = $10.00
currencystringYesISO 4217 code
serial_numberstringYes (Cloud API)PAX terminal serial number
terminal_typestringYes (Cloud API)Terminal model, e.g. "PAXA920"
transactionReferencestringNoUUID v4 for idempotency and status queries

Errors​

CodeMeaningRecovery
DECLINEDIssuer declinedAsk cardholder to provide another card
CANCELLEDOperator cancelled on terminalNo action required
MOTO_NOT_ENABLEDMOTO not enabled for this merchantContact Handpoint team

Remote Sale (MOTO)​

Back-office · charges a stored card tokenAndroid (PAX)CordovaBackoffice

A card-not-present sale submitted directly to the gateway using a stored card token — no terminal or card reader required. The ISV system sends the charge via REST API or Android SDK using a token previously issued by a supported token provider.

When to use it​

Use for recurring billing, subscription charges, or any scenario where you hold a card-on-file token from a prior tokenization or card-present transaction. A card token must already exist before this operation can be sent.

How to obtain a card token​

MethodHowWhen to use
Sale & Tokenize (saleAndTokenizeCard)Charges the card and issues a token in one stepOnly when getting the token alongside the sale is a hard requirement — if the tokenization step fails, the entire authorisation also fails
Tokenize Only (tokenizeCard)No charge — reads the card and stores a tokenLoyalty enrolment, "save my card" flows, or any time you need a token without a payment
Deferred — GET token after sale (recommended)Do a regular sale, then call GET /transactions/{id}/token (Backoffice REST, no terminal needed)Preferred for recurring billing — decouples the token from the sale; a tokenisation failure doesn't affect the transaction result

The deferred approach is recommended because the card-present sale completes independently. You call the backoffice token endpoint afterwards and store the token for future card-not-present charges.

No raw card data — tokens only

Handpoint back-office MOTO does not accept raw PAN, expiry, or CVV from the ISV. All charges use a cardToken issued by a supported provider (e.g. Paysafe, Tokenex). The token provider de-tokenizes at processing time — the ISV never handles card data.

Address Verification (AVS)​

EPI supports an optional billing object on MOTO Sale — postal code (and optionally street address) are forwarded to the acquirer to reduce CNP fraud risk. Requires avsForMoto enabled per merchant by Handpoint.

POST https://cloud.handpoint.com/moto/sale
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"amount": "33.09",
"currency": "USD",
"cardToken": "YOUR_STORED_CARD_TOKEN",
"transactionReference": "c1e1a7ee-1432-4a9c-9171-414e203dbb49",
"billing": {
"zipCode": "10001",
"address": "123 Main St"
}
}

address is optional — zipCode alone is a valid billing object. Both are personal data; handle per your retention and masking policy.

info

See AVS for prerequisites, full field reference, and edge cases.

Code​

Back-office sale — no terminal required, amount in major currency units. Synchronous — the result is returned immediately; no polling or callback URL needed.

POST https://cloud.handpoint.com/moto/sale
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"amount": "10.00",
"currency": "USD",
"cardToken": "YOUR_STORED_CARD_TOKEN",
"transactionReference": "538f1ee7-9f6f-49b7-8a49-89f7cc3aaad9"
}

Parameters​

Backoffice (POST /moto/sale):

NameTypeRequiredDescription
amountstringYesAmount in major currency units — "10.00" = $10.00 (note: different from with-reader which uses minor units)
currencystringYesISO 4217 code
cardTokenstringYesToken from a supported provider — never a raw PAN
transactionReferencestringRecommendedUUID v4. Send on every original transaction — required to recover the outcome via GET /transactions/{reference}/status/all when no result is received or when finStatus: UNDEFINED is returned. See Transaction Recovery.
customerReferencestringNoMerchant reference forwarded to the acquirer

Android SDK MoToOptions fields:

FieldTypeDescription
cardTokenString?Card token for back-office MOTO — omit to show terminal entry screen instead
channelMoToChannel?MAIL_ORDER or TELEPHONE_ORDER
customerReferenceString?Merchant reference echoed in TransactionResult

Errors​

CodeMeaningRecovery
DECLINEDIssuer declinedRequest another payment method from the customer
INVALID_TOKENToken not recognised or expiredVerify token and token provider match acquirer
MOTO_NOT_ENABLEDMOTO not provisioned for this merchantContact Handpoint team

Sale with Tip​

On-device · tip collected at checkoutCloud APIAndroid (PAX)Android (HiLite)CordovaJavaScript SDKWindows (.NET)

Configure a tip prompt on the terminal as part of the sale — the cardholder selects a tip amount before completing payment. This is distinct from Tip Adjustment (which adds a tip after the sale is authorised).

When to use it​

Use in hospitality environments (restaurants, taxis, salons) where tipping is expected at point of sale. The tip is collected at the terminal, included in the authorised amount, and settled together with the base sale — no second operation is needed.

Code​

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "sale",
"amount": "1000",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"tipConfiguration": {
"baseAmount": "1000",
"tipPercentages": [10, 15, 20],
"enterAmountEnabled": true,
"skipEnabled": true,
"footer": "Thank you!"
},
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f"
}

baseAmount is used to calculate the percentage amounts displayed. enterAmountEnabled: true lets the cardholder type a custom tip. skipEnabled: true adds a "No tip" option.

TipConfiguration fields​

FieldTypeDescription
baseAmountstring / BigIntegerBase sale amount used to calculate percentage tip values shown on screen
tipPercentagesarray of integersTip percentage options to display, e.g. [10, 15, 20]
enterAmountEnabledbooleantrue to show a "Custom amount" entry option
skipEnabledbooleantrue to show a "No tip / Skip" option
footerstringOptional message shown at the bottom of the tip screen

Result fields​

FieldDescription
tipAmountTip amount chosen by the cardholder (minor units)
totalAmountBase amount + tip — what was authorised and will settle

Pre-selected tip (ISV-collected)​

Use this variant when your application has already collected the tip from the cardholder — for example, your POS shows a custom tip screen and the cardholder selects a tip amount before the card is presented. Pass TipConfiguration(tipAmount) with the pre-determined amount; the terminal skips its own tip-selection screen and charges base + tip in a single authorisation.

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "sale",
"amount": "1000",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"tipConfiguration": {
"tipAmount": "500"
},
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f"
}

The terminal charges amount + tipAmount in a single authorisation. No tip-selection screen is shown on the terminal.

Tip Adjustment vs Sale with Tip

Sale with Tip collects the tip at the terminal before authorisation — the total (base + tip) is authorised in one step. Tip Adjustment adds a tip after an already-authorised sale, updating the settlement amount. Use Sale with Tip when the cardholder is at the terminal; use Tip Adjustment for tip-at-table flows where you capture a signature and enter the tip later.

Errors​

CodeMeaningRecovery
DECLINEDIssuer declined (after tip selection)Ask cardholder to try another card
CANCELLEDCardholder cancelled at tip screen or payment screenNo action required — no amount was authorised
TIMEOUTTerminal did not respondCheck connection; retry
COMMUNICATION_ERRORNetwork failureVerify connectivity; retry

Edge cases​

ScenarioBehaviour
Cardholder skips tiptipAmount is 0 or absent in the result; totalAmount equals the base amount
baseAmount differs from sale amountTip percentages are calculated on baseAmount — use this intentionally (e.g. to exclude tax from the tip base)
Pre-selected tip + card declineFull amount + tipAmount was attempted; no partial capture — treat as a standard decline

Testing​

Test on the TEST/DEMO merchant or staging device.

ScenarioHow to trigger
Tip from percentageSet tipPercentages: [10, 15, 20] — select a percentage at terminal — verify tipAmount and totalAmount in result
Custom tipSet enterAmountEnabled: true — enter a custom amount — verify totalAmount = baseAmount + entered tip
Skip tipSet skipEnabled: true — select No Tip — verify tipAmount: 0, totalAmount equals base
Pre-selected tipPass TipConfiguration(tipAmount: 500) — verify terminal skips tip screen, totalAmount = amount + 500
Cardholder cancelsCancel at tip screen — verify CANCELLED and no charge

Sale with Tokenization​

On-device · stores card token for future chargesCloud APIAndroid (PAX)Android (HiLite)iOS (HiLite)CordovaJavaScript SDKWindows (.NET)

Store a reusable card token during the sale so future card-not-present charges can be made without the cardholder being present again. The token is returned in the TransactionResult and can be used for subsequent back-office MOTO sales.

When to use it​

Use when onboarding a new customer in person — take the first payment as a normal card-present sale and simultaneously capture a card token for future recurring charges, subscriptions, or card-on-file billing.

Code​

Tokenization happens as a separate operation via the /transactions endpoint with "operation": "tokenizeCard". The resulting token can then be used in back-office MOTO sales.

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "tokenizeCard",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f"
}

The token is returned in the TransactionResult.cardToken field. Store it and use it in subsequent POST /moto/sale requests.

Result fields​

FieldDescription
cardTokenOpaque token string issued by the acquirer's token provider. Store this — do not log or expose it.
finStatusAUTHORISED on success

Using the token for future charges​

Pass cardToken in the Remote Sale (MOTO) flavor's cardToken field for subsequent card-not-present charges.

Token scope

Tokens are specific to the merchant and acquirer configuration. A token issued on a test/staging merchant cannot be used on production, and vice versa. Token validity periods vary by acquirer — consult your acquirer documentation.

Refund​

EMV Refund​

On-device · card present at terminalCloud APIAndroid (PAX)Android (HiLite)iOS (HiLite)CordovaJavaScript SDKWindows (.NET)

Returns funds to a cardholder's account for a card-present transaction. The cardholder must present their payment method at the terminal.

Linked refund (recommended): includes originalTransactionId — the gateway validates the original transaction and caps the refund at the original amount. Include partial amounts for partial refunds.

Unlinked refund: omit originalTransactionId — sends a standalone credit without referencing the original sale. Some acquirers restrict unlinked refunds; check your acquirer agreement.

Same payment method

Per merchant configuration, the same card used in the original sale must be presented. Physical card inserts and mobile wallet taps (Apple Pay, Google Pay) produce different PAN tokens — if same-card verification is enabled and the original sale was a physical insert, a wallet tap at refund time will decline. Advise the cardholder to use the same payment method they used at purchase.

When to use it​

Use for returns after settlement has occurred. For cancellations of unsettled transactions, use Reversal — it is faster and incurs no interchange fees.

Code​

Linked refund (no transactionReference — subsequent operation):

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "refund",
"amount": "1000",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"originalTransactionId": "01236fc0-8192-11eb-9aca-ad4b0e95f241"
}

Unlinked refund (include transactionReference — original operation):

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "refund",
"amount": "1000",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"transactionReference": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Parameters​

NameTypeRequiredDescription
operationstringYes (Cloud API)"refund"
amountstringYesRefund amount in smallest currency unit. Can be less than original for partial refund
currencystringYesISO 4217 code — must match original sale
serial_numberstringYes (Cloud API)Target terminal serial number
terminal_typestringYes (Cloud API)Terminal model
originalTransactionIdstringLinked onlytransactionID from the original sale result. Do NOT combine with transactionReference
transactionReferencestringUnlinked only (Cloud API)UUID v4 — include on unlinked refunds; omit on linked refunds

Errors​

CodeMeaningRecovery
DECLINEDAmount exceeds original or issuer declinedReduce amount to at most original totalAmount; contact acquirer support
ORIGINAL_NOT_FOUNDoriginalTransactionId not foundVerify the GUID is the transactionID from the sale result, not eFTTransactionID
UNLINKED_REFUND_NOT_ALLOWEDAcquirer does not permit unlinked refundsAlways include originalTransactionId for this acquirer
TIMEOUTTerminal did not respondCheck connection; retry
COMMUNICATION_ERRORNetwork failureVerify connectivity; retry

Edge cases​

ScenarioBehaviour
Partial refundSend less than the original totalAmount — acquirer caps at the original amount; do not exceed it
Multiple partial refundsEach linked refund reduces the refundable balance; sending more than the cumulative remaining amount will decline
Wallet vs physical cardIf same-card verification is enabled, Apple Pay / Google Pay taps produce different PAN tokens from a physical card insert — the cardholder must use the same payment method as the original sale
Refund of a tipped saleInclude the full totalAmount (base + tip) if refunding the whole transaction; a partial refund should target the base only unless the acquirer allows tip refunds
Already settledLinked refunds work post-settlement — this is the correct path after batch close

Testing​

Test on the TEST/DEMO merchant or staging device.

ScenarioHow to trigger
Full linked refundComplete a sale, then send a refund with originalTransactionId for the full amount — verify AUTHORISED
Partial linked refundSend refund for less than the original amount — verify AUTHORISED and correct totalAmount
Over-amount linked refundSend refund for more than the original amount — verify DECLINED
Unlinked refundOmit originalTransactionId, include transactionReference — verify acquirer allows it

Remote Refund (MOTO)​

Back-office · linked to original MOTO saleAndroid (PAX)CordovaJavaScript SDKWindows (.NET)Backoffice

A card-not-present refund submitted directly to the gateway — no terminal or card reader required. References the original MOTO sale by its GUID. Valid up to 12 months after the original sale date.

When to use it​

Use to refund a previously processed back-office MOTO or key-entry sale. The cardholder does not need to be present or provide their card again — the gateway retrieves the card from the original transaction.

Code​

Synchronous — the result is returned immediately. No terminal, no polling.

POST https://cloud.handpoint.com/moto/refund
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"amount": "10.00",
"currency": "USD",
"originalGuid": "01236fc0-8192-11eb-9aca-ad4b0e95f241"
}

originalGuid is the efttransactionID (or transactionID) from the original MOTO sale result. Amount is in major currency units — "10.00" = $10.00. Do not include transactionReference on refunds (linked operation).

Parameters​

Backoffice (POST /moto/refund):

NameTypeRequiredDescription
amountstringYesRefund amount in major currency units — "10.00" = $10.00
currencystringYesISO 4217 code
originalGuidstringYesefttransactionID from the original MOTO sale. The gateway retrieves the card token from the original transaction — do not pass cardToken on refunds

Errors​

CodeMeaningRecovery
DECLINEDAmount exceeds original or issuer declinedReduce to at most the original amount
ORIGINAL_NOT_FOUNDoriginalGuid not foundVerify the GUID is from the MOTO sale result
MOTO_NOT_ENABLEDMOTO not provisionedContact Handpoint team

Key Entry Refund​

On-device · operator keys card numberCloud APIAndroid (PAX)JavaScript SDKWindows (.NET)

Commands a PAX terminal to display a manual card entry screen — the cashier types the cardholder's card number, expiry, and CVV directly on the terminal's touchscreen. The terminal processes the refund as a MOTO transaction. No original transaction ID required.

Processed as MOTO — acquirer fees apply

Key Entry Refund is submitted to the acquirer as a MOTO (card-not-present) transaction. MOTO transactions typically carry higher interchange rates. Confirm the fee structure with your acquirer during merchant onboarding.

Card data stays on the terminal

Unlike back-office MOTO refund (which uses a stored card token), key entry refund requires the PAX terminal to be physically present. The operator's system sends the command; the terminal's screen collects the card details.

When to use it​

Use when a cardholder requests a refund for a key-entry or phone order, and you do not have a stored card token. The PAX terminal must be present and running in integrated mode.

Code​

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "moToRefund",
"amount": "1000",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f"
}

Amount in smallest currency unit. The terminal shows a card entry screen — the operator types card details on device. Result is async — poll GET /transaction-result/{transactionResultId}.

Parameters​

NameTypeRequiredDescription
operationstringYes (Cloud API)"moToRefund" — triggers card entry screen on terminal
amountstringYesSmallest currency unit — "1000" = $10.00
currencystringYesISO 4217 code
serial_numberstringYes (Cloud API)PAX terminal serial number
terminal_typestringYes (Cloud API)Terminal model, e.g. "PAXA920"
transactionReferencestringNoUUID v4 for idempotency and status queries

Errors​

CodeMeaningRecovery
DECLINEDIssuer declinedAsk cardholder to provide another card
CANCELLEDOperator cancelled on terminalNo action required
MOTO_NOT_ENABLEDMOTO not enabled for this merchantContact Handpoint team

Reversal​

Reversal​

On-device · no card requiredCloud APIAndroid (PAX)Android (HiLite)iOS (HiLite)CordovaJavaScript SDKWindows (.NET)

What this does​

Cancels an unsettled transaction before batch settlement — the full authorised amount is released to the cardholder without interchange fees. No card present required.

When to use it​

Use when a transaction needs to be cancelled before the batch closes (same business day, before cut-off time). This is always preferable to a post-settlement Refund — faster and fee-free. Do NOT use after settlement — send a Refund instead.

Implementation notes​

  1. Same business day only. Reversal works on the current open batch only. Once the batch closes (cut-off time, typically end of business day), the transaction settles and only a post-settlement Refund is available.
  2. No card present required. Reversal references the original transactionID electronically — the cardholder does not need to return to the terminal.
  3. Terminal must be online. The SDK paths route the reversal through the connected reader — the terminal must be reachable at the time of the call.

Code​

Routes through the connected terminal SDK — the reversal is processed by the device and appears in the device app transaction history. Asynchronous — poll GET /transaction-result/{transactionResultId} for the outcome. Amount in minor units.

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "saleReversal",
"amount": "1100",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "0821599465",
"originalTransactionId": "f2075470-9194-11f1-90c9-a73194216a3b"
}

Response:

{
"statusMessage": "Operation Accepted",
"transactionResultId": "0821599465-1786020446467"
}

Partial Reversal​

What this does​

Reduces the authorised amount on a sale before settlement — the cardholder is charged only the reduced amount.

When to use it​

Use when the final amount is lower than the authorised amount and the batch is still open. Example: a pre-authorised $100 hotel stay costs only $80 at checkout. Do NOT attempt on closed batches — send a Refund instead.

Implementation notes​

  1. Requires TMS enablement per merchant. Contact your Handpoint integration engineer to enable partial reversal for a specific merchant account.
  2. The partial reversal uses the MasterCard flag in ISO 8583 internally.
  3. The result is non-cumulative — you cannot chain multiple partial reversals. The amount you send is the new final authorised amount, not a delta.
  4. Available via Cloud API and Android SDK (PAX). Not supported on HiLite or iOS paths.

Code​

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "saleReversal",
"amount": "800",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"originalTransactionId": "01236fc0-8192-11eb-9aca-ad4b0e95f241"
}

Send amount less than the original authorised amount. The gateway recognises this as a partial reversal. Do not include a transactionReference — this is a subsequent operation linked to the original sale's reference.

Parameters​

NameTypeRequiredDescription
operationstringYes (Cloud API)Must be "saleReversal"
amountstringYesNew final amount as a string (not a delta). Must be less than original authorised amount
currencystringYesISO 4217 currency code — must match the original sale
serial_numberstringYes (Cloud API)Target terminal serial number
terminal_typestringYes (Cloud API)Terminal model, e.g. "PAXA920"
originalTransactionIdstringYestransactionID from the original sale result

Errors​

CodeMeaningRecovery
AMOUNT_EXCEEDS_ORIGINALNew amount is greater than originalCorrect the amount
OPERATION_NOT_SUPPORTEDPartial reversal not enabled for this merchantContact Handpoint to enable in TMS
ORIGINAL_NOT_FOUNDReference not foundVerify reference

Testing​

Test partial reversals on the staging device (PAX debug) or TEST/DEMO merchant. Amount must be less than the original authorised amount. Verify that partial reversal is enabled for your test merchant before testing.

Parameters​

Cloud API — POST /transactions:

NameTypeRequiredDescription
operationstringYesMust be "saleReversal"
amountstringYesFull original sale amount in minor units as a string, e.g. "1100" = $11.00
currencystringYesISO 4217 currency code — must match the original sale
terminal_typestringYesTerminal model, e.g. "PAXA920"
serial_numberstringYesTerminal serial number
originalTransactionIdstringYestransactionID from the original sale result
callbackUrlstringNoURL to receive the transaction result via HTTP POST when complete
tokenstringNoOpaque value included alongside the push notification. Only used when callbackUrl is set

Android SDK / iOS SDK:

NameTypeDescription
amountBigIntegerFull original sale amount in minor currency units
currencyCurrencyCurrency of the original sale
originalTransactionIDStringtransactionID from the original sale result

Errors​

Errors arrive in the polled result object, not in the initial 202. Check finStatus — do not parse statusMessage for programmatic logic as it can be localized.

finStatusstatusMessageMeaningWhat to do
DECLINEDUNABLE_TO_FIND_MESSAGE_TO_REVERSE.originalTransactionId not found in the open batchVerify the GUID; if the batch has closed, send a Refund instead
FAILEDTransaction failed, error: Error getting advanced transaction status...originalTransactionId not found (pre-auth reversal path)Verify the GUID is from the pre-auth create result

Immediate HTTP errors (returned before the 202):

HTTPmessageMeaningWhat to do
403No valid key found in headerInvalid API keyCheck ApiKeyCloud header value
400{"error":1001,"message":"Device is busy"}Terminal is processing another operationWait and retry; implement a short backoff

Testing​

Test reversals on the TEST/DEMO merchant or staging device.

ScenarioHow to trigger
SuccessComplete a sale, send POST /transactions same day — verify AUTHORISED via GET /transaction-result/{id}
After cut-offClose the batch, attempt reversal — verify error and route to Refund
Already reversedSend the same reversal twice — verify idempotent or ALREADY_REVERSED response

Remote Reversal​

Back-office · no reader requiredCloud APIBackoffice
Cloud API vs. Backoffice — what's the difference?
PathHow it worksTransaction history
Cloud APIRoutes through the connected terminal SDK — the device processes the reversalAppears in device app transaction history
BackofficeSends directly to the payment gateway — no terminal or SDK involvedAppears in your POS/solution only — not in the device app

Both paths use the same ApiKeyCloud credential.

What this does​

Cancels any unsettled transaction — sale, MOTO, pre-auth capture, or refund — without the cardholder returning to the terminal.

When to use it​

  • Cloud API path: Use when you have a terminal online and want the reversal recorded in the device transaction history.
  • Backoffice path: Use when the terminal is unavailable, or when the original transaction was fully server-side (MOTO, pre-auth via Cloud API). No MOTO or TMS enablement required for full reversals.

Implementation notes​

  1. Backoffice amount is in major units (decimal string) — e.g. "11.00" for $11.00. Cloud API uses minor units (integer string).
  2. Omit amount and currency for a full reversal. Include them only for a partial reversal (EPI only — requires TMS enablement per merchant).
  3. Do NOT include a transactionReference on either path — this is a subsequent operation linked to the original.
  4. Interac cards (Paysafe + Interac gateway only): Remote reversal is not available for Interac card transactions. Interac requires card-present at terminal. Use the on-device Reversal flow instead. Non-Interac cards on the same gateway work normally.

Code​

Routes through the connected terminal SDK. Asynchronous — poll GET /transaction-result/{transactionResultId} for the outcome. The reversal appears in the device app transaction history.

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "saleReversal",
"amount": "1100",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "0821599465",
"originalTransactionId": "f2075470-9194-11f1-90c9-a73194216a3b"
}

Response:

{
"statusMessage": "Operation Accepted",
"transactionResultId": "0821599465-1786020446467"
}

For parameters, async result polling, and error codes — see the Reversal section above.

Parameters​

Cloud API — POST /transactions: see Reversal section for the full parameter list.

Backoffice — POST /reversal:

NameTypeRequiredDescription
originalGuidstringYestransactionID from the original transaction result (any type)
amountstringNoNew final amount in major units as a decimal string, e.g. "8.00". Omit for a full reversal. Required when currency is provided
currencystringNoISO 4217 currency code. Required when amount is provided

Errors​

Errors arrive in the polled result — see Reversal — Errors.

Testing​

Same test scenarios as on-device Reversal — see Reversal — Testing.

Tip Adjustment​

Post-authorization · adjust tip before batch closeAndroid (PAX)Android (HiLite)CordovaBackoffice

Modifies the tip amount on a completed sale before batch close. Used in restaurant/hospitality flows where the cardholder signs a paper receipt and adds a tip after the initial authorization.

Full guide

For acquirer restrictions, batch-close deadline, cross-SDK usage, iOS HiLite specifics, and the Sale with Tip vs Tip Adjustment comparison, see the Tipping Guide.

Tip adjustment + partial refund ordering

If you need to perform a partial refund on a transaction that already has a tip adjustment, follow this sequence:

  1. Send a $0 tip adjustment to zero out the existing tip
  2. Perform the partial refund
  3. Send a new tip adjustment for the correct tip amount on the remaining balance

Why: This acquirer's backend links the tip to the original authorization amount. A partial refund against a tipped transaction can produce incorrect settlement figures unless the tip is zeroed first. Sending a new tip adjustment after the refund restores the correct tip on the adjusted balance.

EPI

EPI (formerly TSYS). Batch auto-closes ~11pm EST (host-capture). Partial reversal available for US and Canada — uses the MasterCard flag in ISO 8583. Tip adjustment only available on transactions processed without an on-screen tip (tip-at-table flow).

Code​

Synchronous — the gateway responds immediately. No terminal, no polling.

POST https://cloud.handpoint.com/transactions/01236fc0-8192-11eb-9aca-ad4b0e95f241/tip-adjustment
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"amount": 9.00
}

The transactionID from the original sale goes in the URL path. To void a tip, send "amount": 0.

Parameters​

Backoffice — POST /transactions/{transactionID}/tip-adjustment:

NameTypeRequiredDescription
{transactionID}string (path)YestransactionID from the original sale result
amountnumberYesTip amount in major currency units (e.g. 9.00 for $9.00). 0 to void an existing tip

Android SDK — tipAdjustment(tipAmount, currency, originalTransactionID):

ParameterTypeDescription
tipAmountBigIntegerTip amount in minor units. BigInteger("0") to void
currencyCurrencyCurrency of the original transaction
originalTransactionIDStringtransactionID from the original sale result

Response​

{
"statusMessage": "tip adjusted",
"batchNumber": "123"
}
FieldTypeDescription
statusMessagestring"tip adjusted" on success
batchNumberstringThe current open batch number, if returned by the acquirer. May be absent

Errors​

CodeMeaningRecovery
ORIGINAL_NOT_FOUNDTransaction ID not foundVerify the ID
BATCH_ALREADY_CLOSEDBatch has closedRefund the tip amount instead
TIP_ADJUSTMENT_NOT_ENABLEDTMS config not setContact Handpoint onboarding team
AMOUNT_EXCEEDS_LIMITTip exceeds allowed percentageVerify tip amount

Testing​

Test tip adjustment on the TEST/DEMO merchant or staging device. Tip adjustment is enabled by default — no additional configuration required. See the Tipping guide for full details on tip-at-table flows.

Pre-Authorization​

Pre-Auth Create​

On-device · chip, contactless, or magstripeCloud APIAndroid (PAX)CordovaJavaScript SDKWindows (.NET)

What this does​

Places a hold on funds without capturing them, allowing the final amount to be adjusted before settlement.

When to use it​

Use for hotel check-ins, car rentals, or any flow where the final amount is unknown at the time of card interaction. Do NOT use for standard retail — use Sale instead.

Pre-Auth is a bundle of operations​

Supporting pre-auth means supporting the full lifecycle:

OperationDescription
CreatePlace the initial hold
Increase / DecreaseAdjust the held amount before capture — see the Pre-Authorization Guide
CaptureFinalise and charge the held amount
Pre-Auth ReversalRelease the hold without charging (pre-capture)
Capture ReversalCancel a completed capture (pre-settlement) — see separate section

Implementation notes​

  1. A pre-auth creates an authorisation hold on the cardholder's account. Funds are not captured until you send a Capture.
  2. The hold typically expires after 7–30 days depending on the card network and issuer. Always capture or void before expiry.
  3. Increase / Decrease adjusts the hold with a delta, not a new total, and always references the original pre-auth transactionID. Not available on every acquirer — see the Pre-Authorization Guide.
  4. Always reverse unused pre-authorisations — unreleased holds affect the cardholder's available credit.

Code​

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "preAuthorization",
"amount": "10000",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f"
}

Parameters​

NameTypeRequiredDescription
operationstringYes (Cloud API)Must be "preAuthorization"
amountstringYesAuthorisation amount in smallest currency unit as a string
currencystringYesISO 4217 currency code
serial_numberstringYes (Cloud API)Target terminal serial number
terminal_typestringYes (Cloud API)Terminal model, e.g. "PAXA920"
transactionReferencestringNo (Cloud API)UUID v4. Send on Pre-Auth create. Do not send on Capture, Void, or Increase. Enables status queries and groups all lifecycle operations
Android SDK — multi-MID

The Android SDK preAuthorization() accepts an optional MerchantAuthOptions (not plain Options) to override the merchant ID and terminal ID per acquirer. See Authentication for details.

Errors​

CodeMeaningRecovery
DECLINEDIssuer declined the holdAsk cardholder to try another card
CANCELLEDCardholder cancelled at terminalNo action required
TIMEOUTTerminal did not respondCheck connection; retry
COMMUNICATION_ERRORNetwork failureVerify connectivity; retry

Edge cases​

ScenarioBehaviour
Hold expiryHolds typically expire after 7–30 days (issuer / network dependent) — always capture or void before the expiry window; an expired hold cannot be captured
Capture amount exceeds holdMost acquirers allow slight overages (e.g. hotel incidentals); exceed the allowed threshold and the capture will decline
Unreleased holdFailing to void unused pre-auths affects the cardholder's available balance — always void if you will not capture
Multiple increasesEach Increase adjusts the hold by a delta — confirm acquirer supports multiple increases before implementation

Testing​

ScenarioHow to trigger
Create holdSend with transactionReference — verify finStatus: AUTHORISED and no amount settled yet
Full lifecycleCreate → Capture → query /status/all — verify both operations in chain
Release without captureCreate → Void — verify hold released, no settlement

Key Entry Pre-Auth​

On-device · operator keys card numberCloud APIAndroid (PAX)CordovaJavaScript SDKWindows (.NET)

Places a MOTO pre-authorization hold by commanding a PAX terminal to display a manual card entry screen. Reserves funds without charging — the ISV captures, increases, or voids before settlement.

When to use: For hotel or car rental reservations taken over the phone where the cardholder cannot be present. The operator keys the card number on the PAX terminal's manual entry screen.

Processed as MOTO — acquirer fees apply

Key Entry Pre-Auth is submitted to the acquirer as a MOTO (card-not-present) transaction. MOTO transactions typically carry higher interchange rates. Confirm the fee structure with your acquirer during merchant onboarding.

Implementation notes:

  1. The key entry pre-auth flow supports the full pre-auth lifecycle: Increase / Decrease → Capture → Void.
  2. PAX terminal required — HiLite has no keyed-entry screen and does not support MOTO pre-auth on either path.

Code​

Commands a PAX terminal to display a manual card entry screen. The operator types the card number, expiry, and CVV. Amount in smallest currency unit. Result is async — poll GET /transaction-result/{transactionResultId}.

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "moToPreAuthorization",
"amount": "10000",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"transactionReference": "3cfe2fd3-34c2-5d78-a4e0-2e5b668f5e5f"
}

Store transactionResultId from the 202 response, poll GET /transaction-result/{transactionResultId} until finStatus is set, then store transactionID from the result for capture / increase / reversal.

Parameters​

ParameterTypeRequiredDescription
operationstringYes (Cloud API)Must be "moToPreAuthorization"
amountstring / BigIntegerYesAmount in smallest currency unit — "10000" = $100.00
currencystring / CurrencyYesISO 4217 currency code
terminal_typestringYes (Cloud API)Terminal model, e.g. "PAXA920"
serial_numberstringYes (Cloud API)PAX terminal serial number
transactionReferencestringRecommendedUUID v4 for idempotency

Testing​

ScenarioHow to trigger
Key entry pre-authSend moToPreAuthorization — terminal shows keyed-entry screen, verify AUTHORISED
CaptureKey entry pre-auth → send preAuthorizationCapture with originalGuid
VoidKey entry pre-auth → send preAuthorizationReversal with originalTransactionId

Pre-Auth Capture​

No card required — settle the held amountAndroid (PAX)CordovaJavaScript SDKWindows (.NET)Backoffice

Captures the funds from a previously created pre-authorization (completion).

When to use: When you know the final amount and are ready to settle. Send the actual charged amount — it may differ from the original pre-auth amount.

Implementation notes:

  1. The capture amount can be less than the pre-auth amount. Most acquirers also allow a small overage (check your acquirer rules).
  2. You must capture before the hold expires (typically 7–30 days).
  3. After capture, a standard refund process applies if the cardholder requests a return.

Code​

Direct to the gateway — no terminal required. Synchronous — result returned immediately.

POST https://cloud.handpoint.com/preauthorization/capture
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"originalGuid": "01236fc0-8192-11eb-9aca-ad4b0e95f241",
"capturedAmount": "95.00"
}

originalGuid is the transactionID from the pre-authorization result.

Backoffice — POST /preauthorization/capture:

ParameterTypeRequiredDescription
originalGuidstringYestransactionID from the pre-authorization result (UUID v4)
capturedAmountstringYesCapture amount in major currency units as a decimal string, e.g. "95.00" for $95.00
tipAmountstringNoOptional tip amount in major currency units as a decimal string

Android SDK — preAuthorizationCapture(amount, currency, originalTransactionID, options?):

ParameterTypeDescription
amountBigIntegerCapture amount in minor currency units
currencyCurrencyCurrency of the original pre-authorization
originalTransactionIDStringtransactionID from the pre-authorization result
optionsOptions?Optional — supports customerReference
note

The Android SDK preAuthorizationCapture() does not accept a tipAmount parameter. tipAmount is a Cloud API-only field.

Errors​

Backoffice (POST /preauthorization/capture) — synchronous, errors are immediate:

HTTPcodemessageMeaningWhat to do
4005001NullPointerExceptionoriginalGuid not foundVerify the GUID is the transactionID from the pre-auth create result
4004066Partial reversal amount exceeds original amountcapturedAmount exceeds the pre-auth hold amountReduce the capture amount
4003051Already reversedPre-auth was already voidedCheck your records — void and capture are mutually exclusive
4003211(pre-auth already settled)Pre-auth has already been capturedVerify state via the TXN Feed API; if the first capture succeeded, issue a refund if needed
403—No valid key found in headerInvalid API keyCheck ApiKeyCloud header
422VALIDATION_FAILEDMissing required fieldRequest body is missing originalGuid or capturedAmountSee details array in response

Testing​

ScenarioHow to trigger
Capture full hold amountcapturedAmount = original Pre-Auth amount — verify AUTHORISED
Capture partial amountcapturedAmount < original amount — verify settled amount in portal
Capture with tipInclude tipAmount — verify total settled = capturedAmount + tipAmount
Invalid originalGuidUse a random UUID — verify ORIGINAL_NOT_FOUND

Pre-Auth Reversal​

Release the hold without chargingCloud APIAndroid (PAX)CordovaJavaScript SDKWindows (.NET)

Releases a pre-authorization hold without capturing funds.

When to use: When a booking is cancelled or the pre-auth is no longer needed. Always reverse unused pre-auths — unreleased holds affect cardholder available credit.

Timing — not same-day restricted

Reversing a pre-auth hold is not subject to the same-day cut-off that applies to sale reversals. You can reverse the hold at any point before it expires — typically 7–30 days from the original authorization, depending on the card network and issuer. After expiry the hold is released automatically by the network. See Pre-Authorization Hold Durations for card-brand rules.

Code​

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "preAuthorizationReversal",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"originalTransactionId": "01236fc0-8192-11eb-9aca-ad4b0e95f241"
}
ParameterTypeRequiredDescription
operationstringYes (Cloud API)Must be "preAuthorizationReversal"
serial_numberstringYes (Cloud API)Target terminal serial number
terminal_typestringYes (Cloud API)Terminal model, e.g. "PAXA920"
originalTransactionIdstringYestransactionID from the pre-authorization result

Errors​

Void is a with-reader operation — errors arrive in the polled result, not in the initial 202 response.

finStatusMeaningWhat to do
DECLINED / FAILEDoriginalTransactionId not foundVerify the GUID is the transactionID from the pre-auth create result
DECLINEDPre-auth has already been captured or voidedCheck transaction state before voiding
note

There is no separate "already voided" error code — attempting to void an already-captured or already-voided pre-auth returns a DECLINED or FAILED result. Always check the finStatus field in the polled result.

Testing​

ScenarioHow to trigger
Void before captureCreate Pre-Auth → send Void — verify AUTHORISED and no settlement in portal
Void after captureCreate → Capture → Void — verify whether acquirer supports post-capture void
Invalid referenceUse a random originalTransactionId — verify ORIGINAL_NOT_FOUND

Pre-Auth Capture Reversal​

Void a captured pre-authorization — releases the chargeCloud APIAndroid (PAX)Backoffice

Pre-Authorization Capture Reversal​

Reverses a completed pre-authorization capture — releasing the funds withheld and returning the transaction to AUTHORISED state.

When to use: When a pre-auth was captured in error and the merchant needs to fully cancel the capture before overnight settlement.

How it differs from Pre-Auth Reversal: Pre-Auth Reversal releases the hold before capture. Capture Reversal cancels the capture after it has been accepted.

Implementation notes:

  1. Must be sent before the batch closes / overnight settlement. After settlement, only a Refund is possible.
  2. Full capture amount only — partial capture reversals are not supported.
  3. Not all acquirers support this operation — check the capabilities table.
  4. Android SDK: The same preAuthorizationReversal() method is used for both voiding the hold (pre-capture) and reversing the capture (post-capture). The gateway determines the correct action based on the state of the original transaction.
POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "preAuthorizationReversal",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"originalTransactionId": "01236fc0-8192-11eb-9aca-ad4b0e95f241"
}

Errors​

The HTTP 202 is always returned immediately — errors appear only in the polled result. Do not parse statusMessage for programmatic logic — it can be localized; check finStatus.

finStatusstatusMessageMeaningWhat to do
DECLINEDUNABLE_TO_FIND_MESSAGE_TO_REVERSE.originalTransactionId not found, already reversed, or capture already settledVerify the GUID is the transactionID from the pre-auth capture result; if already settled, issue a Refund instead
DECLINEDALREADY_REVERSEDCapture was already reversedNo further action; confirm via GET /transaction-result/{transactionResultId}
FAILEDTransaction failed, error: Error getting advanced transaction status (transaction not found)...Gateway could not locate the original transactionVerify originalTransactionId — use the transactionID from the capture result, not the pre-auth create
FAILED(other)Gateway or terminal errorCall GET /transaction-result/{transactionResultId} for the full errorMessage; retry if transient

Immediate HTTP errors (returned before the 202):

HTTPmessageMeaningWhat to do
403No valid key found in headerInvalid or missing API keyCheck the ApiKeyCloud header value
400{"error":1001,"message":"Device is busy"}Terminal is busyWait for the current operation to finish, then retry
400{"error":1002,"message":"Device not found"}serial_number not enrolled or incorrectVerify serial_number matches the registered terminal
400TransactionReference with wrong uuidv4 format ...transactionReference not a valid UUID v4Generate a compliant UUID v4 (version digit 4, variant 8/9/a/b)
400originalTransactionId is requiredMissing required fieldInclude originalTransactionId in the request body

Tokenization​

Cygma Token​

EPI · Cygma vault — MOTO detokenization by gateway; ISV stays out of PCI scopeCloud APIAndroid (PAX)Android (HiLite)iOS (HiLite)CordovaBackoffice

What this does​

Reads the card at the terminal and stores it in the Cygma token vault — a PCI-compliant service operated by EPI. The vault stores the PAN and expiry date (no CVV). Handpoint returns a cardToken your system can store for future card-not-present charges.

When to use it​

Use when you want to charge a returning customer without requiring them to present their card again — subscriptions, recurring billing, "save my card" flows. The ISV stores only the opaque token; the PAN never leaves the EPI vault, keeping you out of PCI scope.

How detokenization works​

When you send a MOTO sale with a Cygma cardToken, the Handpoint Gateway retrieves the PAN from the vault internally and forwards the authorisation to EPI — the ISV never sees the raw card number. Your system handles tokens only.

Implementation notes​

  1. Token contents: PAN + expiry date stored; CVV is never stored (PCI requirement).
  2. Token scope: Cygma tokens are specific to the EPI merchant configuration — a token from one merchant cannot be used on another, and test tokens are not valid in production.
  3. Deferred tokenization (recommended): Retrieve a token from a previously completed transaction — no card re-swipe needed. Via Backoffice REST (GET /transactions/{id}/token) — no terminal required, synchronous. Via Android SDK (hapi.deferredTokenization(), SDK 7.1013.1+) — requires PAX device connected.
  4. Future charges: Use the cardToken in Remote Sale (MOTO) — Backoffice POST /moto/sale or Android SDK hapi.motoSale().

Code​

Routes through the connected terminal (requires terminal_type and serial_number). Asynchronous — poll or use callback for the result.

Tokenize only (no charge):

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "tokenizeCard",
"terminal_type": "PAXA920",
"serial_number": "082104578"
}

Sale and tokenize (charge + token in one step):

POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"operation": "saleAndTokenizeCard",
"amount": "1000",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f"
}

Parameters​

Card-present tokenization:

NameTypeRequiredDescription
operationstringYes (Cloud API)"tokenizeCard" for tokenize-only, "saleAndTokenizeCard" for sale+tokenize
amountstringYes (saleAndTokenizeCard)Amount in smallest currency unit as a string
currencystringYes (saleAndTokenizeCard)ISO 4217 currency code
serial_numberstringYes (Cloud API)Target terminal serial number
terminal_typestringYes (Cloud API)Terminal model, e.g. "PAXA920"

Deferred tokenization — Android SDK (hapi.deferredTokenization(eftTransactionID)):

ParameterTypeDescription
eftTransactionIDStringEFTTransactionID (UUID) from the original transaction — NOT transactionID
ReturnsBooleantrue if command sent to device; result arrives via endOfTransaction

Deferred tokenization — Backoffice (GET /transactions/{transactionID}/token):

ParameterDescription
{transactionID}transactionID (GUID) from the original card-present transaction

Response fields​

FieldDescription
cardTokenOpaque Cygma token. Store this — use for future MOTO charges
cardTokenProviderToken issuer — "EPI" or "PROCHARGE"
expiryDateMMYYToken expiry (matches card expiry)

Errors​

CodeMeaningRecovery
TOKENIZATION_NOT_ENABLEDNot configured for this merchantContact Handpoint team
CARD_READ_ERRORTerminal failed to read cardRetry; ask customer to try again

Testing​

Test tokens are returned on the TEST/DEMO merchant or staging device. Test tokens are not valid in production and cannot be used in POST /moto/sale on a live merchant.

Batch Operations​

Manually close or query the current settlement batchBackoffice

Three Backoffice REST API endpoints for batch management — calls go directly to the payment gateway, no terminal or SDK involved.

When to use it​

Use when you need to settle at a specific time rather than waiting for automatic batch close. Most US host-capture merchants use automatic batch close (~11pm EST) — manual close is only needed for specific settlement timing requirements.

Implementation notes​

  1. US host-capture acquirers only. EU acquirers use automatic settlement and do not require manual batch close.
  2. After batch close, tip adjustments and reversals are no longer possible — only post-settlement refunds.
  3. Auto-close is the default for US host-capture merchants at approximately 11pm EST. Manual close is an alternative, not an addition.
EPI

EPI (formerly TSYS). Batch auto-closes ~11pm EST (host-capture). Partial reversal available for US and Canada — uses the MasterCard flag in ISO 8583. Tip adjustment only available on transactions processed without an on-screen tip (tip-at-table flow).

Batch Close​

Closes the current open batch and triggers settlement with the acquirer. Omit batchNumber to close the currently open batch.

POST https://cloud.handpoint.com/batch/close
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"serialNumber": "082104578",
"deviceType": "PAXA920"
}
{
"httpStatus": "200",
"batchNumber": "132",
"batchStatus": "CLOSED",
"closeBatchGuid": "d1988a50-49fb-11f1-b64d-2969d719a012",
"closedAt": "20260928214051578",
"customFields": {
"entry": {
"key": "issuerBatchCloseLocalTimestamp",
"value": "2026-09-28T02:40:51"
}
},
"issuerResponseCode": "00",
"issuerResponseText": "ACCEPTED"
}

Batch Summary​

Retrieves aggregate totals for a batch — transaction count and net amounts by type.

POST https://cloud.handpoint.com/batch/summary
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"serialNumber": "082104578",
"deviceType": "PAXA920",
"batchNumber": "132"
}

netAmount is in minor units (e.g. 644397 = $6,443.97). transactionCount includes all transaction types; salesCount and refundsCount are in customFields. closedAt is only present when batchStatus is "CLOSED".

{
"httpStatus": "200",
"batchNumber": "133",
"transactionCount": "12",
"netAmount": "644397",
"issuerResponseCode": "00",
"issuerResponseText": "DATA RETRIEVED",
"customFields": {
"entry": [
{ "key": "salesCount", "value": "11" },
{ "key": "refundsCount", "value": "1" }
]
},
"batchSummaryGuid": "1ef0c830-bbe4-11f1-9efa-074a901f9b3c",
"batchStatus": "OPEN"
}

Batch Detail​

Retrieves individual transactions in a batch for reconciliation. Results are paginated: each call returns at most 5 transactions, in descending order (most recent first). The page size is fixed and cannot be configured.

Pagination:

  1. On the first call, omit retrievalReferenceNumber. The response contains the 5 most recent transactions of the batch, most recent first.
  2. To get the next page (older transactions), send the same request with retrievalReferenceNumber set to the retrievalReferenceNumber of the last (oldest) item in details.
  3. Repeat until details is empty or missing.
Do not stop on a short page

A page can contain fewer than 5 items even when older transactions remain: the gateway leaves out transaction types that it does not report (for example voids). Stop only when details is empty or missing.

let cursor;
const transactions = [];
do {
const res = await batchDetail({ ...request, retrievalReferenceNumber: cursor });
const page = [].concat(res.details ?? []);
transactions.push(...page);
cursor = page.at(-1)?.retrievalReferenceNumber;
} while (cursor);
POST https://cloud.handpoint.com/batch/detail
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json

{
"serialNumber": "082104578",
"deviceType": "PAXA920",
"batchNumber": "132"
}
{
"httpStatus": "200",
"batchNumber": "132",
"closedAt": "20260928214051578",
"issuerResponseCode": "00",
"issuerResponseText": "DATA RETRIEVED",
"details": [
{ "transactionType": "SALE", "retrievalReferenceNumber": "627121800445", "amount": "1200" },
{ "transactionType": "SALE", "retrievalReferenceNumber": "627121800444", "amount": "1002" },
{ "transactionType": "SALE", "retrievalReferenceNumber": "627121800443", "amount": "21000" },
{ "transactionType": "SALE", "retrievalReferenceNumber": "627121800442", "amount": "27000" },
{ "transactionType": "SALE", "retrievalReferenceNumber": "627121800441", "amount": "26000" }
],
"batchDetailGuid": "f02e9130-bbe3-11f1-9efa-074a901f9b3c",
"customFields": {
"entry": { "key": "issuerBatchCloseLocalTimestamp", "value": "2026-09-28T02:40:51" }
},
"batchStatus": "CLOSED"
}

Response fields​

FieldDescription
details[].transactionTypeSALE, REFUND, TIP_ADJUSTMENT, PREAUTHORIZATION_INCREASE or PREAUTHORIZATION_CAPTURE. A pre-authorization is reported as SALE — the gateway does not distinguish it in the batch detail.
details[].retrievalReferenceNumberRRN of the transaction. Items are in descending order (most recent first), so the last item of the page is the oldest: use its RRN as the cursor for the next page.
details[].amountIn minor units. For a CLOSED batch it is the settled amount; for an OPEN batch it is the authorized amount. PREAUTHORIZATION_INCREASE always shows the increase amount and PREAUTHORIZATION_CAPTURE always shows the captured amount.

Notes​

  • Open batches: new transactions go to the top of the batch, so they do not appear in the pages that follow. To include them, start again without retrievalReferenceNumber. The cursor is an RRN, not a page number, so you never get duplicates when new transactions arrive.
  • Reconciliation: Batch Detail does not list every transaction type. To check totals, use Batch Summary.

Parameters​

NameTypeRequiredDescription
serialNumberstringYesTerminal serial number
deviceTypestringYesTerminal model, e.g. "PAXA920"
batchNumberstringNoBatch number to close or query. If omitted, the gateway resolves the terminal's current open batch
retrievalReferenceNumberstringNoBatch Detail only. Pagination cursor: pass the retrievalReferenceNumber of the last (oldest) item in the previous page to get the next page. Omit it to get the most recent page

Testing​

Test batch close on the staging device (PAX debug). Batch operations are server-side REST API calls — no card interaction required.