PAYSAFE
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows SDK
Integration Capabilities
| Capability | Cloud API | Native PAX | HiLite | Back Office |
|---|---|---|---|---|
| Sale | ✓ | ✓ | ✓ | — |
| Refund | ✓ | ✓ | ✓ | — |
| Reversal | ✓ | ✓ | ✓ | ✓ |
| Tip Adjustment | ✓ | ✓ | ✓ | ✓ |
| Pre-Authorization | ✓ | ✓ | — | ✓ |
| MOTO | ✓ | ✓ | — | — |
| Tokenization | ✓ | ✓ | ✓ | — |
| Void | ✓ | ✓ | ✓ | — |
| Interac | ✓ | ✓ | — | — |
✓ Supported — Not available on this path
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
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows (.NET)
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.
hapi.sale(BigInteger("1000"), Currency.USD)
override fun endOfTransaction(result: TransactionResult, device: Device) {
if (result.finStatus == FinancialStatus.AUTHORISED) {
// store result.transactionID for any subsequent reversal
}
}
hapi.sale(BigInteger("1000"), Currency.USD)
override fun endOfTransaction(result: TransactionResult, device: Device) {
if (result.finStatus == FinancialStatus.AUTHORISED) {
// store result.transactionID for any subsequent reversal
}
}
heftClient.saleWithAmount(1000, currency: "USD")
func responseFinanceStatus(_ info: (any FinanceResponseInfo)!) {
if info.finStatus() == "AUTHORISED" {
// store info.eFTTransactionID() for potential reversal
}
}
handpoint.sale(
{ amount: 1000, currency: handpoint.Currency.USD },
function(result) {
if (result.finStatus === "AUTHORISED") {
// store result.transactionID for potential reversal
}
},
function(error) { console.error("Sale failed:", error); }
);
// @handpoint/cloud-js-sdk
const { transactionReference, transactionResult } = hp.sale(
1000, // smallest currency unit — 1000 = $10.00
'USD',
{
terminalType: 'PAXA920',
serialNumber: '082104578',
customerReference: 'order-5248',
}
);
// Persist transactionReference before awaiting — needed for recovery if connection drops
const result = await transactionResult;
if (result.finStatus === 'AUTHORISED') {
// result.transactionID for subsequent reversal
}
using System.Numerics;
using com.handpoint.api;
var op = hapi.Sale(new BigInteger(1000), Currency.USD);
if (!op.OperationStarted)
{
// SDK rejected the command — log op.ErrorMessage and handle in UI
return;
}
// Persist op.TransactionReference before the result arrives
string transactionRef = op.TransactionReference;
// Result delivered via Events.Required callback:
public void EndOfTransaction(TransactionResult result, Device device)
{
if (result.FinStatus == FinancialStatus.AUTHORISED)
{
// store result.TransactionID for any subsequent reversal
}
}
Parameters
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows (.NET)
| Name | Type | Required | Description |
|---|---|---|---|
operation | string | Yes | Must be "sale" |
amount | string | Yes | Smallest currency unit as a string — "1000" = $10.00 |
currency | string | Yes | ISO 4217 code, e.g. "USD", "EUR", "CAD" |
terminal_type | string | Yes | Terminal model, e.g. "PAXA920" |
serial_number | string | Yes | Target terminal serial number |
transactionReference | string | No | UUID v4 — send on original transactions only; omit on reversals and linked refunds |
customerReference | string | No | Merchant reference forwarded to the acquirer |
callbackUrl | string | No | URL to receive the result via POST; if omitted, poll GET /transaction-result/{id} |
token | string | No | Opaque value included with the push notification. Only used when callbackUrl is set |
taxInformation | object | No | Level 2 tax data: taxAmount (string, minor units) and taxExempt (boolean). taxAmount is required: "0" when taxExempt is true, greater than "0" otherwise, and never above amount. Must be sent with purchaseOrderNumber |
purchaseOrderNumber | string | No | Level 2 purchase order number. Must be sent with taxInformation. Alphanumeric, max 25 characters |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Amount in smallest currency unit. BigInteger("1000") = $10.00 |
currency | Currency | Yes | Currency enum, e.g. Currency.USD |
options | SaleOptions? | No | Optional — see fields below |
SaleOptions fields:
| Field | Type | Description |
|---|---|---|
customerReference | String? | Merchant reference echoed in TransactionResult and forwarded to the acquirer |
tipConfiguration | TipConfiguration? | Pre-configure tip prompt on the terminal (percentages, amounts, or skip) |
budgetNumber | String? | Instalment / budget number (South African acquirers only) |
pinBypass | Boolean | true to offer PIN bypass where acquirer-supported |
signatureBypass | Boolean | true to skip signature capture where acquirer-supported |
checkDuplicates | Boolean | true to enable duplicate detection on the gateway |
merchantAuth | MerchantAuth? | Override MID / TID for multi-MID merchants |
metadata | Metadata? | Custom key-value pairs echoed in result and stored in txn feed |
moneyRemittanceOptions | MoneyRemittanceOptions? | Fields for regulated money transfer transactions |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Amount in smallest currency unit. BigInteger("1000") = $10.00 |
currency | Currency | Yes | Currency enum, e.g. Currency.USD |
options | SaleOptions? | No | Optional — same fields as Android PAX above |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | NSInteger | Yes | Amount in smallest currency unit. 1000 = $10.00 |
currency | NSString * | Yes | ISO 4217 currency code as string, e.g. @"USD" |
| Name | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount in smallest currency unit. 1000 = $10.00 |
currency | string | Yes | Currency constant, e.g. handpoint.Currency.USD |
customerReference | string | No | Merchant reference forwarded to the acquirer |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount in smallest currency unit. 1000 = $10.00 |
currency | string | Yes | ISO 4217 code, e.g. 'USD' |
options.terminalType | string | Yes | Terminal model, e.g. 'PAXA920' |
options.serialNumber | string | Yes | Terminal serial number |
options.customerReference | string | No | Merchant reference forwarded to the acquirer |
options.transactionReference | string | No | UUID v4 — send on original transactions only |
options.callbackUrl | string | No | URL to receive the result via POST |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Amount in smallest currency unit. new BigInteger(1000) = $10.00 |
currency | Currency | Yes | Currency enum, e.g. Currency.USD |
options | SaleOptions? | No | Optional — same fields as Android PAX above |
Errors
| Code | Meaning | Recovery |
|---|---|---|
DECLINED | Issuer declined | Ask cardholder to try another card |
CANCELLED | Cardholder cancelled at terminal | No action required |
TIMEOUT | Terminal did not respond | Check connection; retry |
COMMUNICATION_ERROR | Network failure | Verify connectivity; retry |
PARTIAL_APPROVAL | Issuer approved a lesser amount | Accept partial amount or reverse; see Partial Approval |
Edge cases
| Scenario | Behaviour |
|---|---|
| Contactless limit exceeded | Terminal falls back to chip insert — instruct the cardholder to insert their card |
| Card chip read failure | Terminal retries up to 3 times, then offers magstripe fallback — acquirer support for swipe varies |
| Duplicate detection | If checkDuplicates is enabled and the same card + amount is seen within the window, the gateway rejects the second transaction |
| Connection drops mid-sale | Persist 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.
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows (.NET)
| Scenario | How to trigger |
|---|---|
| Approved | Send a valid request — verify finStatus: AUTHORISED, note transactionID for reversal tests |
| Declined | Use acquirer test card for decline — verify finStatus: DECLINED |
| Partial approval | Use acquirer test card for partial approval — verify finStatus: PARTIAL_APPROVAL and that your app handles it |
| Timeout recovery | Drop connectivity mid-transaction — query GET /transactions/{transactionReference}/status/all to confirm outcome |
| Scenario | How to trigger |
|---|---|
| Approved | hapi.sale() — verify AUTHORISED in endOfTransaction |
| Declined | Use test card for decline — verify DECLINED and that no reversal is triggered |
| Tap → insert fallback | Hold contactless card above limit — verify terminal prompts for insertion |
Same as Android PAX.
| Scenario | How to trigger |
|---|---|
| Approved | heftClient.saleWithAmount:currency: — verify AUTHORISED in responseFinanceStatus |
| Declined | Use test card for decline — verify callback receives DECLINED |
| Scenario | How to trigger |
|---|---|
| Approved | handpoint.sale() — verify finStatus === "AUTHORISED" in success callback |
| Declined | Use test card — verify DECLINED |
| Scenario | How to trigger |
|---|---|
| Approved | hp.sale() — verify finStatus === 'AUTHORISED' |
| Timeout recovery | Drop network after initiating — use transactionReference to query status |
| Scenario | How to trigger |
|---|---|
| Approved | hapi.Sale() — verify AUTHORISED in EndOfTransaction |
| SDK rejection | Pass invalid amount — verify op.OperationStarted == false and log op.ErrorMessage |
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.
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.
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
- Cloud API
- Android (PAX)
- JavaScript SDK
- Windows (.NET)
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.
// Terminal shows manual card entry screen — no cardToken needed
hapi.motoSale(BigInteger("1000"), Currency.USD)
override fun endOfTransaction(result: TransactionResult, device: Device) {
if (result.finStatus == FinancialStatus.AUTHORISED) {
// store result.transactionID for potential reversal
}
}
const { transactionReference, transactionResult } = hp.moToSale(1000, 'USD');
const result = await transactionResult;
if (result.finStatus === 'AUTHORISED') {
// store result.transactionID for potential reversal
}
Terminal displays the manual card entry screen — the operator types card details on the device.
var op = hapi.MoToSale(new BigInteger(1000), Currency.USD);
if (!op.OperationStarted) { /* handle connection error */ return; }
// Result delivered via EndOfTransaction callback
public void EndOfTransaction(TransactionResult result, Device device)
{
if (result.FinStatus == FinancialStatus.AUTHORISED) {
// store result.TransactionID for potential reversal
}
}
Parameters
- Cloud API
- Android (PAX)
- JavaScript SDK
- Windows (.NET)
| Name | Type | Required | Description |
|---|---|---|---|
operation | string | Yes | Must be "moToSale" — triggers card entry screen on terminal |
amount | string | Yes | Amount in smallest currency unit — "1000" = $10.00 |
currency | string | Yes | ISO 4217 code, e.g. "USD" |
terminal_type | string | Yes | Terminal model, e.g. "PAXA920" |
serial_number | string | Yes | PAX terminal serial number |
transactionReference | string | No | UUID v4 for idempotency and status queries |
taxInformation | object | No | Level 2 tax data: taxAmount (string, minor units) and taxExempt (boolean). taxAmount is required: "0" when taxExempt is true, greater than "0" otherwise, and never above amount. Must be sent with purchaseOrderNumber |
purchaseOrderNumber | string | No | Level 2 purchase order number. Must be sent with taxInformation. Alphanumeric, max 25 characters |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Amount in smallest currency unit, e.g. BigInteger("1000") = $10.00 |
currency | Currency | Yes | Currency enum, e.g. Currency.USD |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount in smallest currency unit, e.g. 1000 = $10.00 |
currency | string | Yes | ISO 4217 code, e.g. 'USD' |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Amount in smallest currency unit, e.g. new BigInteger(1000) = $10.00 |
currency | Currency | Yes | Currency enum, e.g. Currency.USD |
Errors
| Code | Meaning | Recovery |
|---|---|---|
DECLINED | Issuer declined | Ask cardholder to provide another card |
CANCELLED | Operator cancelled on terminal | No action required |
MOTO_NOT_ENABLED | MOTO not enabled for this merchant | Contact 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
| Method | How | When to use |
|---|---|---|
Sale & Tokenize (saleAndTokenizeCard) | Charges the card and issues a token in one step | Only 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 token | Loyalty 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.
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.
Code
- Backoffice
- Android (PAX)
- Cordova
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"
}
// Back-office MOTO — pass cardToken in options, no terminal entry screen. Asynchronous — result arrives in endOfTransaction
val options = MoToOptions()
options.cardToken = "YOUR_STORED_CARD_TOKEN"
hapi.motoSale(BigInteger("1000"), Currency.USD, options)
override fun endOfTransaction(result: TransactionResult, device: Device) {
if (result.finStatus == FinancialStatus.AUTHORISED) {
// store result.transactionID
}
}
// Android only
handpoint.motoSale(
{ amount: 1000, currency: handpoint.Currency.USD },
function(result) {
if (result.finStatus === "AUTHORISED") { /* accepted */ }
},
function(error) { console.error(error); }
);
Parameters
- Backoffice
- Android (PAX)
- Cordova
Amount in major currency units — "10.00" = $10.00. Synchronous — result returned immediately in the response body.
| Name | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | Amount in major currency units — "10.00" = $10.00. Must match ^\d+(\.\d+)?$, otherwise 422; within that, at most 12 digits, or 13 characters with a decimal point, no leading zeros except a single 0 before the decimal point, as in 0.50, otherwise 400 |
currency | string | Yes | ISO 4217 code, e.g. "USD" |
cardToken | string | Yes | Card-on-file token from a supported provider — never a raw PAN |
transactionReference | string | Recommended | UUID v4 for idempotency and recovery via GET /transactions/{reference}/status/all |
customerReference | string | No | Merchant reference forwarded to the acquirer |
taxInformation | object | No | Level 2 tax data: taxAmount (string, major currency units, e.g. "1.00"; at most 12 digits, or 13 characters with a decimal point, no leading zeros except a single 0 before the decimal point, as in 0.50) and taxExempt (boolean). taxAmount is required: "0" when taxExempt is true, greater than "0" otherwise, and never above amount. Must be sent with purchaseOrderNumber |
purchaseOrderNumber | string | No | Level 2 purchase order number. Must be sent with taxInformation. Alphanumeric, max 25 characters |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Amount in smallest currency unit, e.g. BigInteger("1000") = $10.00 |
currency | Currency | Yes | Currency enum, e.g. Currency.USD |
options | MoToOptions? | No | Optional — see fields below |
MoToOptions fields:
| Field | Type | Description |
|---|---|---|
cardToken | String? | Card-on-file token for back-office MOTO — omit to show terminal card entry screen |
channel | MoToChannel? | MAIL_ORDER or TELEPHONE_ORDER |
customerReference | String? | Merchant reference echoed in TransactionResult |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount in smallest currency unit, e.g. 1000 = $10.00 |
currency | string | Yes | Currency constant, e.g. handpoint.Currency.USD |
Errors
| Code | Meaning | Recovery |
|---|---|---|
DECLINED | Issuer declined | Request another payment method from the customer |
INVALID_TOKEN | Token not recognised or expired | Verify token and token provider match acquirer |
MOTO_NOT_ENABLED | MOTO not provisioned for this merchant | Contact Handpoint team |
400 (validation) | Invalid amount format, or invalid taxInformation / purchaseOrderNumber | Fix the field — see Level 2 validation errors |
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
- Cloud API
- Android (PAX)
- Android (HiLite)
- Cordova
- JavaScript SDK
- Windows (.NET)
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.
val tipConfig = TipConfiguration(
baseAmount = BigInteger("1000"),
headerName = "Tip",
tipPercentages = listOf(5, 10, 15, 20),
enterAmountEnabled = true,
skipEnabled = true,
footer = "Thank you!"
)
val options = SaleOptions()
options.tipConfiguration = tipConfig
hapi.sale(BigInteger("1000"), Currency.USD, options)
override fun endOfTransaction(result: TransactionResult, device: Device) {
if (result.finStatus == FinancialStatus.AUTHORISED) {
// result.tipAmount — tip selected by cardholder
// result.totalAmount — base + tip
}
}
val tipConfig = TipConfiguration()
tipConfig.baseAmount = BigInteger("1000")
tipConfig.tipPercentages = listOf(5, 10, 15, 20)
tipConfig.isEnterAmountEnabled = true
tipConfig.isSkipEnabled = true
val options = SaleOptions()
options.tipConfiguration = tipConfig
hapi.sale(BigInteger("1000"), Currency.USD, options)
handpoint.sale(
{
amount: 1000,
currency: handpoint.Currency.USD,
tipConfiguration: {
baseAmount: 1000,
tipPercentages: [10, 15, 20],
enterAmountEnabled: true,
skipEnabled: true
}
},
function(result) {
if (result.finStatus === "AUTHORISED") {
console.log("Tip:", result.tipAmount, "Total:", result.totalAmount);
}
},
function(error) { console.error(error); }
);
// @handpoint/cloud-js-sdk
const { transactionReference, transactionResult } = hp.sale(
1000,
'USD',
{
terminalType: 'PAXA920',
serialNumber: '082104578',
tipConfiguration: {
baseAmount: 1000,
tipPercentages: [10, 15, 20],
enterAmountEnabled: true,
skipEnabled: true,
},
}
);
const result = await transactionResult;
if (result.finStatus === 'AUTHORISED') {
console.log('Tip:', result.tipAmount, 'Total:', result.totalAmount);
}
var tipConfig = new TipConfiguration
{
BaseAmount = new BigInteger(1000),
TipPercentages = new List<int> { 10, 15, 20 },
EnterAmountEnabled = true,
SkipEnabled = true,
Footer = "Thank you!"
};
var options = new SaleOptions { TipConfiguration = tipConfig };
var op = hapi.Sale(new BigInteger(1000), Currency.USD, options);
public void EndOfTransaction(TransactionResult result, Device device)
{
if (result.FinStatus == FinancialStatus.AUTHORISED)
{
// result.TipAmount — tip chosen by cardholder
// result.TotalAmount — base + tip
}
}
TipConfiguration fields
| Field | Type | Description |
|---|---|---|
baseAmount | string / BigInteger | Base sale amount used to calculate percentage tip values shown on screen |
tipPercentages | array of integers | Tip percentage options to display, e.g. [10, 15, 20] |
enterAmountEnabled | boolean | true to show a "Custom amount" entry option |
skipEnabled | boolean | true to show a "No tip / Skip" option |
footer | string | Optional message shown at the bottom of the tip screen |
Result fields
| Field | Description |
|---|---|
tipAmount | Tip amount chosen by the cardholder (minor units) |
totalAmount | Base 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.
- Cloud API
- Android (PAX)
- Android (HiLite)
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.
// ISV collected $5.00 tip on their own screen — pass it directly
val tipConfig = TipConfiguration(BigInteger("500")) // terminal skips its tip-selection screen
val options = SaleOptions()
options.tipConfiguration = tipConfig
hapi.sale(BigInteger("1000"), Currency.USD, options)
// Cardholder is charged $15.00 total (base $10.00 + pre-selected tip $5.00)
override fun endOfTransaction(result: TransactionResult, device: Device) {
if (result.finStatus == FinancialStatus.AUTHORISED) {
// result.tipAmount → 500 (the pre-set tip)
// result.totalAmount → 1500 (base + tip)
}
}
val tipConfig = TipConfiguration(BigInteger("500"))
val options = SaleOptions()
options.tipConfiguration = tipConfig
hapi.sale(BigInteger("1000"), Currency.USD, options)
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
| Code | Meaning | Recovery |
|---|---|---|
DECLINED | Issuer declined (after tip selection) | Ask cardholder to try another card |
CANCELLED | Cardholder cancelled at tip screen or payment screen | No action required — no amount was authorised |
TIMEOUT | Terminal did not respond | Check connection; retry |
COMMUNICATION_ERROR | Network failure | Verify connectivity; retry |
Edge cases
| Scenario | Behaviour |
|---|---|
| Cardholder skips tip | tipAmount is 0 or absent in the result; totalAmount equals the base amount |
baseAmount differs from sale amount | Tip percentages are calculated on baseAmount — use this intentionally (e.g. to exclude tax from the tip base) |
| Pre-selected tip + card decline | Full amount + tipAmount was attempted; no partial capture — treat as a standard decline |
Testing
Test on the TEST/DEMO merchant or staging device.
| Scenario | How to trigger |
|---|---|
| Tip from percentage | Set tipPercentages: [10, 15, 20] — select a percentage at terminal — verify tipAmount and totalAmount in result |
| Custom tip | Set enterAmountEnabled: true — enter a custom amount — verify totalAmount = baseAmount + entered tip |
| Skip tip | Set skipEnabled: true — select No Tip — verify tipAmount: 0, totalAmount equals base |
| Pre-selected tip | Pass TipConfiguration(tipAmount: 500) — verify terminal skips tip screen, totalAmount = amount + 500 |
| Cardholder cancels | Cancel at tip screen — verify CANCELLED and no charge |
Sale with Paysafe Token
On-device · captures a Paysafe single-use token for Customer Vault APICloud APIAndroid (PAX)Android (HiLite)iOS (HiLite)CordovaJavaScript SDKWindows (.NET)
Charge the card and simultaneously capture a Paysafe single-use token — a short-lived, one-time credential that lets you securely pass card data to Paysafe's Customer Vault API without your server ever handling the raw PAN.
This token is intended for use on Paysafe's Customer Vault API, not Handpoint's Remote Sale (MOTO) endpoint. Handpoint Gateway does not detokenize Paysafe single-use tokens for MOTO processing.
Contact the Paysafe team before building a token-based flow to confirm the token format Handpoint returns meets your integration requirements.
When to use it
Use when onboarding a customer in person — take the first payment as a normal card-present sale and simultaneously capture a Paysafe single-use token. Your system then calls the Paysafe Customer Vault API to vault the card and receive a permanent paymentToken for future recurring charges.
Code
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows (.NET)
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"
}
The cardToken in the result is a Paysafe single-use token — pass it to the Paysafe Customer Vault API. It is not for use with Handpoint's /moto/sale endpoint.
val options = SaleAndTokenizeOptions()
hapi.sale(BigInteger("1000"), Currency.USD, options)
override fun endOfTransaction(result: TransactionResult, device: Device) {
if (result.finStatus == FinancialStatus.AUTHORISED) {
val token = result.cardToken // Paysafe single-use token
val provider = result.cardTokenProvider // "PAYSAFE"
// Pass token to Paysafe Customer Vault API — not to Handpoint MOTO
}
}
val options = SaleAndTokenizeOptions()
hapi.sale(BigInteger("1000"), Currency.USD, options)
override fun endOfTransaction(result: TransactionResult, device: Device) {
val token = result.cardToken // Paysafe single-use token
}
// Sale with tokenization — token returned in result.cardToken
heftClient.saleWithAmount(1000, currency: "USD", cardIsPresent: true)
// Access token: result.cardToken (Paysafe single-use)
handpoint.saleAndTokenizeCard(
{ amount: 1000, currency: handpoint.Currency.USD },
function(result) {
if (result.finStatus === "AUTHORISED") {
const token = result.cardToken; // Paysafe single-use token
// Pass to Paysafe Customer Vault API
}
},
function(error) { console.error(error); }
);
const { transactionResult } = hp.saleAndTokenizeCard(
1000,
'USD',
{ terminalType: 'PAXA920', serialNumber: '082104578' }
);
const result = await transactionResult;
if (result.finStatus === 'AUTHORISED') {
const token = result.cardToken; // Paysafe single-use token
// Pass to Paysafe Customer Vault API — not to Handpoint MOTO
}
var options = new SaleAndTokenizeOptions();
var op = hapi.Sale(new BigInteger(1000), Currency.USD, options);
public void EndOfTransaction(TransactionResult result, Device device)
{
if (result.FinStatus == FinancialStatus.AUTHORISED)
{
string token = result.CardToken; // Paysafe single-use token
string provider = result.CardTokenProvider; // "PAYSAFE"
// Pass token to Paysafe Customer Vault API
}
}
Result fields
| Field | Description |
|---|---|
cardToken | Paysafe single-use token — pass to Paysafe Customer Vault API; not for Handpoint MOTO |
cardTokenProvider | "PAYSAFE" |
finStatus | AUTHORISED on success |
Using the token
Pass the single-use token to Paysafe's Customer Vault API to vault the card and receive a permanent paymentToken for future recurring charges. The single-use token expires in ~15 minutes — vault it promptly.
For implementation details, see the Paysafe Single-Use Token section in Tokenization.
Paysafe tokens are specific to the merchant configuration. Test tokens (from api.test.paysafe.com) are not valid in production.
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.
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
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows (.NET)
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"
}
// Linked refund
hapi.refund(BigInteger("1000"), Currency.USD, "01236fc0-8192-11eb-9aca-ad4b0e95f241")
// Unlinked refund — omit the originalTransactionID argument
hapi.refund(BigInteger("1000"), Currency.USD)
override fun endOfTransaction(result: TransactionResult, device: Device) {
if (result.finStatus == FinancialStatus.AUTHORISED) { /* refund accepted */ }
}
// Linked refund
hapi.refund(BigInteger("1000"), Currency.USD, "01236fc0-8192-11eb-9aca-ad4b0e95f241")
// Unlinked refund
hapi.refund(BigInteger("1000"), Currency.USD)
// Linked refund
heftClient.refundWithAmount(1000, currency: "USD", transaction: "01236fc0-8192-11eb-9aca-ad4b0e95f241")
// Unlinked refund
heftClient.refundWithAmount(1000, currency: "USD")
// Linked refund
handpoint.refund(
{ amount: 1000, currency: handpoint.Currency.USD, originalTransactionID: "01236fc0-8192-11eb-9aca-ad4b0e95f241" },
function(result) { /* handle */ },
function(error) { console.error(error); }
);
// Unlinked refund — omit originalTransactionID
handpoint.refund(
{ amount: 1000, currency: handpoint.Currency.USD },
function(result) { /* handle */ },
function(error) { console.error(error); }
);
// @handpoint/cloud-js-sdk
// Linked refund
const { transactionResult } = hp.refund(
1000,
'USD',
{ originalTransactionId: '01236fc0-8192-11eb-9aca-ad4b0e95f241' }
);
const result = await transactionResult;
if (result.finStatus === 'AUTHORISED') { /* refund accepted */ }
// Unlinked refund — pass a fresh transactionReference; omit originalTransactionId
const { transactionResult: unlinked } = hp.refund(
1000,
'USD',
{ transactionReference: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' }
);
// Linked refund
var op = hapi.Refund(
new BigInteger(1000),
Currency.USD,
"01236fc0-8192-11eb-9aca-ad4b0e95f241" // originalTransactionID
);
// Unlinked refund — omit the originalTransactionID argument
var op = hapi.Refund(new BigInteger(1000), Currency.USD);
public void EndOfTransaction(TransactionResult result, Device device)
{
if (result.FinStatus == FinancialStatus.AUTHORISED) { /* refund accepted */ }
}
Parameters
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows (.NET)
| Name | Type | Required | Description |
|---|---|---|---|
operation | string | Yes | Must be "refund" |
amount | string | Yes | Refund amount in smallest currency unit. Can be less than original for a partial refund |
currency | string | Yes | ISO 4217 code — must match original sale |
terminal_type | string | Yes | Terminal model, e.g. "PAXA920" |
serial_number | string | Yes | Target terminal serial number |
originalTransactionId | string | Linked only | transactionID from the original sale result. Do NOT combine with transactionReference |
transactionReference | string | Unlinked only | UUID v4 — include on unlinked refunds; omit on linked refunds |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Refund amount in smallest currency unit |
currency | Currency | Yes | Currency enum, e.g. Currency.USD |
originalTransactionID | String? | No | transactionID from the original sale — omit for an unlinked refund |
options | RefundOptions? | No | Optional — see fields below |
RefundOptions fields:
| Field | Type | Description |
|---|---|---|
customerReference | String? | Merchant reference echoed in result and forwarded to the acquirer |
checkDuplicates | Boolean | true (default) to enable duplicate detection |
pinBypass | Boolean | true to offer PIN bypass where acquirer-supported |
signatureBypass | Boolean | true to skip signature capture where acquirer-supported |
merchantAuth | MerchantAuth? | Override MID / TID for multi-MID |
metadata | Metadata? | Custom key-value pairs echoed in result and stored in txn feed |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Refund amount in smallest currency unit |
currency | Currency | Yes | Currency enum, e.g. Currency.USD |
originalTransactionID | String? | No | transactionID from the original sale — omit for an unlinked refund |
options | RefundOptions? | No | Optional — same fields as Android PAX above |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | NSInteger | Yes | Refund amount in smallest currency unit |
currency | NSString * | Yes | ISO 4217 currency code as string, e.g. @"USD" |
transaction | NSString * | No | transactionID from the original sale — omit for an unlinked refund |
| Name | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Refund amount in smallest currency unit |
currency | string | Yes | Currency constant, e.g. handpoint.Currency.USD |
originalTransactionID | string | No | transactionID from the original sale — omit for an unlinked refund |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Refund amount in smallest currency unit |
currency | string | Yes | ISO 4217 code, e.g. 'USD' |
options.originalTransactionId | string | Linked only | transactionID from the original sale |
options.transactionReference | string | Unlinked only | UUID v4 — include on unlinked refunds; omit on linked refunds |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Refund amount in smallest currency unit |
currency | Currency | Yes | Currency enum, e.g. Currency.USD |
originalTransactionID | string? | No | transactionID from the original sale — omit for an unlinked refund |
Errors
| Code | Meaning | Recovery |
|---|---|---|
DECLINED | Amount exceeds original or issuer declined | Reduce amount to at most original totalAmount; contact acquirer support |
ORIGINAL_NOT_FOUND | originalTransactionId not found | Verify the GUID is the transactionID from the sale result, not eFTTransactionID |
UNLINKED_REFUND_NOT_ALLOWED | Acquirer does not permit unlinked refunds | Always include originalTransactionId for this acquirer |
TIMEOUT | Terminal did not respond | Check connection; retry |
COMMUNICATION_ERROR | Network failure | Verify connectivity; retry |
Edge cases
| Scenario | Behaviour |
|---|---|
| Partial refund | Send less than the original totalAmount — acquirer caps at the original amount; do not exceed it |
| Multiple partial refunds | Each linked refund reduces the refundable balance; sending more than the cumulative remaining amount will decline |
| Wallet vs physical card | If 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 sale | Include 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 settled | Linked refunds work post-settlement — this is the correct path after batch close |
Testing
Test on the TEST/DEMO merchant or staging device.
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows (.NET)
| Scenario | How to trigger |
|---|---|
| Full linked refund | Complete a sale, then send a refund with originalTransactionId for the full amount — verify AUTHORISED |
| Partial linked refund | Send refund for less than the original amount — verify AUTHORISED and correct totalAmount |
| Over-amount linked refund | Send refund for more than the original amount — verify DECLINED |
| Unlinked refund | Omit originalTransactionId, include transactionReference — verify acquirer allows it |
| Scenario | How to trigger |
|---|---|
| Linked refund | hapi.refund(amount, currency, originalTransactionID) — verify AUTHORISED in endOfTransaction |
| Unlinked refund | hapi.refund(amount, currency) — verify acquirer accepts it |
| Same card check | Use a different card for refund when same-card enforcement is on — verify DECLINED |
Same as Android PAX.
| Scenario | How to trigger |
|---|---|
| Linked refund | heftClient.refundWithAmount:currency:transaction: — verify result in delegate |
| Scenario | How to trigger |
|---|---|
| Linked refund | handpoint.refund() with originalTransactionID — verify AUTHORISED |
| Scenario | How to trigger |
|---|---|
| Linked refund | hp.refund(amount, currency, { originalTransactionId }) — verify finStatus === 'AUTHORISED' |
| Unlinked refund | hp.refund(amount, currency, { transactionReference }) — verify acquirer allows it |
| Scenario | How to trigger |
|---|---|
| Linked refund | hapi.Refund(amount, currency, originalTransactionID) — verify AUTHORISED in EndOfTransaction |
Remote Refund (MOTO)
Back-office · linked to original MOTO saleAndroid (PAX)CordovaBackoffice
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
- Backoffice
- Android (PAX)
- Cordova
- JavaScript SDK
- Windows (.NET)
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).
// MOTO refund — no terminal interaction, submitted via token
hapi.motoRefund(
BigInteger("1000"),
Currency.USD,
"01236fc0-8192-11eb-9aca-ad4b0e95f241" // originalTransactionID from MOTO sale result
)
override fun endOfTransaction(result: TransactionResult, device: Device) {
if (result.finStatus == FinancialStatus.AUTHORISED) { /* refund accepted */ }
}
handpoint.motoRefund(
{
amount: 1000,
currency: handpoint.Currency.USD,
originalTransactionID: "01236fc0-8192-11eb-9aca-ad4b0e95f241"
},
function(result) { /* handle result */ },
function(error) { console.error(error); }
);
// Back-office MOTO refund — no terminal required, submitted via token
const { transactionResult } = hp.moToRefund(
1000,
'USD',
{ originalTransactionID: '01236fc0-8192-11eb-9aca-ad4b0e95f241' }
);
const result = await transactionResult;
if (result.finStatus === 'AUTHORISED') { /* refund accepted */ }
// Back-office MOTO refund — no terminal required, submitted via token
var op = hapi.MoToRefund(
new BigInteger(1000),
Currency.USD,
"01236fc0-8192-11eb-9aca-ad4b0e95f241" // originalTransactionID from MOTO sale result
);
if (!op.OperationStarted) { /* handle connection error */ return; }
public void EndOfTransaction(TransactionResult result, Device device)
{
if (result.FinStatus == FinancialStatus.AUTHORISED) { /* refund accepted */ }
}
Parameters
- Backoffice
- Android (PAX)
- Cordova
- JavaScript SDK
- Windows (.NET)
Amount in major currency units — "10.00" = $10.00. Synchronous — result returned immediately.
| Name | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | Refund amount in major currency units — "10.00" = $10.00 |
currency | string | Yes | ISO 4217 code, e.g. "USD" |
originalGuid | string | Yes | efttransactionID from the original MOTO sale. Gateway retrieves the card token — do not pass cardToken on refunds |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Refund amount in smallest currency unit, e.g. BigInteger("1000") = $10.00 |
currency | Currency | Yes | Currency enum, e.g. Currency.USD |
originalTransactionID | String | Yes | transactionID from the original MOTO sale result |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Refund amount in smallest currency unit, e.g. 1000 = $10.00 |
currency | string | Yes | Currency constant, e.g. handpoint.Currency.USD |
originalTransactionID | string | Yes | transactionID from the original MOTO sale result |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Refund amount in smallest currency unit, e.g. 1000 = $10.00 |
currency | string | Yes | ISO 4217 code, e.g. 'USD' |
options.originalTransactionID | string | Yes | transactionID from the original MOTO sale result |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Refund amount in smallest currency unit, e.g. new BigInteger(1000) = $10.00 |
currency | Currency | Yes | Currency enum, e.g. Currency.USD |
originalTransactionID | string | Yes | transactionID from the original MOTO sale result |
Errors
| Code | Meaning | Recovery |
|---|---|---|
DECLINED | Amount exceeds original or issuer declined | Reduce to at most the original amount |
ORIGINAL_NOT_FOUND | originalGuid not found | Verify the GUID is from the MOTO sale result |
MOTO_NOT_ENABLED | MOTO not provisioned | Contact 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
- 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.
- No card present required. Reversal references the original
transactionIDelectronically — the cardholder does not need to return to the terminal. - 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
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows (.NET)
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"
}
hapi.saleReversal(
BigInteger("1000"),
Currency.USD,
"01236fc0-8192-11eb-9aca-ad4b0e95f241" // originalTransactionID from sale result
)
override fun endOfTransaction(result: TransactionResult, device: Device) {
if (result.finStatus == FinancialStatus.AUTHORISED) {
// reversal accepted — no card prompt, no settlement
}
}
hapi.saleReversal(
BigInteger("1000"),
Currency.USD,
"01236fc0-8192-11eb-9aca-ad4b0e95f241"
)
override fun endOfTransaction(result: TransactionResult, device: Device) {
if (result.finStatus == FinancialStatus.AUTHORISED) {
// reversal accepted
}
}
heftClient.saleVoidWithAmount(1000, currency: "USD", transaction: "01236fc0-8192-11eb-9aca-ad4b0e95f241")
handpoint.saleReversal(
{
amount: 1000,
currency: handpoint.Currency.USD,
originalTransactionID: "01236fc0-8192-11eb-9aca-ad4b0e95f241"
},
function(result) { /* handle result */ },
function(error) { console.error(error); }
);
// @handpoint/cloud-js-sdk
const { transactionResult } = hp.saleReversal(
1000,
'USD',
'01236fc0-8192-11eb-9aca-ad4b0e95f241' // originalTransactionID from sale result
);
const result = await transactionResult;
if (result.finStatus === 'AUTHORISED') {
// reversal accepted — no card prompt, no settlement
}
hapi.SaleReversal(
new BigInteger(1000),
Currency.USD,
"01236fc0-8192-11eb-9aca-ad4b0e95f241" // originalTransactionID from sale result
);
public void EndOfTransaction(TransactionResult result, Device device)
{
if (result.FinStatus == FinancialStatus.AUTHORISED)
{
// reversal accepted — no card prompt, no settlement
}
}
Parameters
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows (.NET)
Routes through the terminal. Amount in minor units (e.g. "1100" = $11.00). Asynchronous — poll GET /transaction-result/{transactionResultId}.
| Name | Type | Required | Description |
|---|---|---|---|
operation | string | Yes | Must be "saleReversal" |
amount | string | Yes | Full original sale amount in minor units as a string, e.g. "1100" = $11.00 |
currency | string | Yes | ISO 4217 currency code — must match the original sale |
terminal_type | string | Yes | Terminal model, e.g. "PAXA920" |
serial_number | string | Yes | Terminal serial number |
originalTransactionId | string | Yes | transactionID from the original sale result |
callbackUrl | string | No | URL to receive the result via HTTP POST. If omitted, poll GET /transaction-result/{transactionResultId} |
token | string | No | Opaque value included with the push notification. Only used when callbackUrl is set |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Full original sale amount in minor currency units |
currency | Currency | Yes | Currency of the original sale, e.g. Currency.USD |
originalTransactionID | String | Yes | transactionID from the original sale result |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Full original sale amount in minor currency units |
currency | Currency | Yes | Currency of the original sale, e.g. Currency.USD |
originalTransactionID | String | Yes | transactionID from the original sale result |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | NSInteger | Yes | Full original sale amount in minor currency units |
currency | NSString * | Yes | ISO 4217 currency code as string, e.g. @"USD" |
transaction | NSString * | Yes | transactionID from the original sale result |
| Name | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Full original sale amount in minor currency units |
currency | string | Yes | Currency constant, e.g. handpoint.Currency.USD |
originalTransactionID | string | Yes | transactionID from the original sale result |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Full original sale amount in minor currency units |
currency | string | Yes | ISO 4217 code, e.g. 'USD' |
originalTransactionID | string | Yes | transactionID from the original sale result |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Full original sale amount in minor currency units |
currency | Currency | Yes | Currency of the original sale, e.g. Currency.USD |
originalTransactionID | string | Yes | transactionID 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.
finStatus | statusMessage | Meaning | What to do |
|---|---|---|---|
DECLINED | UNABLE_TO_FIND_MESSAGE_TO_REVERSE. | originalTransactionId not found in the open batch | Verify the GUID; if the batch has closed, send a Refund instead |
FAILED | Transaction 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):
| HTTP | message | Meaning | What to do |
|---|---|---|---|
403 | No valid key found in header | Invalid API key | Check ApiKeyCloud header value |
400 | {"error":1001,"message":"Device is busy"} | Terminal is processing another operation | Wait and retry; implement a short backoff |
Testing
Test reversals on the TEST/DEMO merchant or staging device.
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows (.NET)
| Scenario | How to trigger |
|---|---|
| Success | Complete a sale, send POST /transactions same day — verify AUTHORISED via GET /transaction-result/{id} |
| After cut-off | Close the batch, attempt reversal — verify error and route to Refund |
| Already reversed | Send the same reversal twice — verify idempotent or ALREADY_REVERSED response |
| Scenario | How to trigger |
|---|---|
| Before cut-off | hapi.saleReversal() same day — verify no card prompt, finStatus == AUTHORISED |
| After cut-off | Close batch, then call hapi.saleReversal() — verify DECLINED or error |
Same as Android PAX.
| Scenario | How to trigger |
|---|---|
| Before cut-off | heftClient.saleVoidWithAmount:currency:transaction: same day — verify success |
| After cut-off | Close batch, attempt void — verify failure in delegate |
| Scenario | How to trigger |
|---|---|
| Before cut-off | handpoint.saleReversal() same day — verify AUTHORISED |
| After cut-off | Close batch, attempt reversal — verify error |
| Scenario | How to trigger |
|---|---|
| Before cut-off | hp.saleReversal() same day — verify finStatus === 'AUTHORISED' |
| After cut-off | Close batch, then attempt reversal — verify error in result |
| Scenario | How to trigger |
|---|---|
| Before cut-off | hapi.SaleReversal() same day — verify FinStatus == AUTHORISED |
| After cut-off | Close batch, then attempt — verify DECLINED or error |
Remote Reversal
Back-office · no reader requiredCloud APIBackoffice
| Path | How it works | Transaction history |
|---|---|---|
| Cloud API | Routes through the connected terminal SDK — the device processes the reversal | Appears in device app transaction history |
| Backoffice | Sends directly to the payment gateway — no terminal or SDK involved | Appears 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
- Backoffice amount is in major units (decimal string) — e.g.
"11.00"for $11.00. Cloud API uses minor units (integer string). - Omit
amountandcurrencyfor a full reversal. Include them only for a partial reversal (EPI only — requires TMS enablement per merchant). - Do NOT include a
transactionReferenceon either path — this is a subsequent operation linked to the original. - 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
- Cloud API
- Backoffice
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.
Sends directly to the payment gateway — no terminal required. Synchronous: the result is returned immediately in the response body. Does not appear in the device app transaction history; only in your POS/solution.
Full reversal:
curl --location --request POST 'https://cloud.handpoint.com/reversal' \
--header 'ApiKeyCloud: YOUR_MERCHANT_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"originalGuid": "bb6e0b90-420f-11f1-b809-51c9c7fda18b"
}'
originalGuid is the transactionID from the original transaction result (any transaction type).
Partial reversal (EPI only — requires TMS enablement per merchant):
curl --location --request POST 'https://cloud.handpoint.com/reversal' \
--header 'ApiKeyCloud: YOUR_MERCHANT_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"originalGuid": "bb6e0b90-420f-11f1-b809-51c9c7fda18b",
"amount": "8.00",
"currency": "USD"
}'
amount is the new final charge — not a delta. Must be less than the original authorised amount. Non-cumulative: you cannot chain partial reversals.
- Success
- Not Found
{
"agreementNumber": "123456789010102",
"cardToken": "665630867",
"cardTokenizationGuid": "7df78050-21dc-11f1-991b-6f80eaf25911",
"expiryDateMMYY": "0927",
"httpStatus": "200",
"maskedCardNumber": "************3555",
"serverDateTime": "20260317083711509",
"transactionReference": "75413c40-21db-11f1-991b-6f80eaf25911"
}
originalGuid not found — no transaction to reverse. Check that the GUID is the transactionID from the original result, not eFTTransactionID.
{
"error": {
"statusCode": 400,
"name": "BadRequestError",
"message": "Viscus operation failed",
"details": {
"status": 404,
"body": {
"error": {
"errorGuid": "c3d26950-bd86-11f1-94eb-17355119bd39",
"reason": "Original transaction not found",
"errorCode": "3158",
"httpStatus": "404"
}
}
}
}
}
Parameters
- Cloud API
- Backoffice
Routes through the terminal — same parameters as the on-device Reversal. Amount in minor units (e.g. "1100" = $11.00).
| Name | Type | Required | Description |
|---|---|---|---|
operation | string | Yes | Must be "saleReversal" |
amount | string | Yes | Original sale amount in smallest currency unit, e.g. "1100" = $11.00 |
currency | string | Yes | ISO 4217 code — must match the original sale |
terminal_type | string | Yes | Terminal model, e.g. "PAXA920" |
serial_number | string | Yes | Terminal serial number |
originalTransactionId | string | Yes | transactionID from the original transaction result |
Direct to the gateway — no terminal required. Amount in major units — "8.00" = $8.00. Synchronous — result returned immediately.
| Name | Type | Required | Description |
|---|---|---|---|
originalGuid | string | Yes | transactionID from the original transaction result (any type) |
amount | string | No | New final amount in major units as a decimal string, e.g. "8.00". Omit for a full reversal. Required when currency is provided |
currency | string | No | ISO 4217 currency code. Required when amount is provided |
Errors
- Cloud API
- Backoffice
Errors arrive in the polled result — see Reversal — Errors.
Synchronous — errors are returned immediately in the response body as a Viscus error object. Relevant fields are in error.details.body.error:
| HTTP | errorCode | reason | Meaning | What to do |
|---|---|---|---|---|
400 | 3158 | Original transaction not found | originalGuid not found | Verify the GUID is the transactionID from the original result (not eFTTransactionID). If the batch has already closed, send a post-settlement Refund instead |
400 | 3051 | Already reversed | Transaction has already been reversed | No further action needed |
400 | 4066 | Partial reversal amount exceeds original amount | amount exceeds the original transaction amount | Omit amount for a full reversal, or reduce below the original amount |
403 | — | No valid key found in header | Invalid API key | Check ApiKeyCloud header |
Testing
- Cloud API
- Backoffice
Same test scenarios as on-device Reversal — see Reversal — Testing.
| Scenario | How to trigger |
|---|---|
| Full reversal — success | Complete a sale, note transactionID, send POST /reversal with originalGuid — verify synchronous httpStatus: "200" |
| After batch close | Close the batch, send reversal — verify 3153 error; route to post-settlement Refund instead |
| Already reversed | Send the same reversal twice — verify 3051 error |
| Partial reversal | Send with amount less than original (EPI with TMS enabled) — verify httpStatus: "200" and reduced charge |
| Pre-auth capture reversal | After a pre-auth capture, send originalGuid = capture transactionID — verify httpStatus: "200" |
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.
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.
If you need to perform a partial refund on a transaction that already has a tip adjustment, follow this sequence:
- Send a
$0tip adjustment to zero out the existing tip - Perform the partial refund
- 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.
PAYSAFE merchants may be onboarded on TSYS, TNS, or both. When a merchant has both processors configured, the Handpoint gateway routes based on card type: Interac cards → TNS (card-present only), all other cards → TSYS. INTERAC VOID: For Interac card transactions, show VOID in your UI — not Refund or Reverse. The only post-sale correction is VOID (full amount, card must be present at terminal). Standard refund is not available for Interac. BATCHING: Batch close applies to TSYS (non-Interac) transactions only. TNS (Interac) transactions settle independently and do not participate in the TSYS batch. Paysafe single-use token: requires merchant onboarding by Paysafe/Handpoint before use.
Code
- Backoffice
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
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.
// tipAdjustment returns Boolean synchronously — no endOfTransaction callback fires
val accepted: Boolean = hapi.tipAdjustment(
BigInteger("900"), // 900 cents = $9.00
Currency.USD,
"01236fc0-8192-11eb-9aca-ad4b0e95f241"
)
// To void a tip: pass BigInteger("0")
// tipAdjustment returns Boolean synchronously — no endOfTransaction callback fires
val accepted: Boolean = hapi.tipAdjustment(
BigInteger("900"), // 900 cents = $9.00
Currency.USD,
"01236fc0-8192-11eb-9aca-ad4b0e95f241"
)
// To void a tip: pass BigInteger("0")
// Objective-C — tipAdjustment is a C function declared in HapiRemoteService.h
#include "HapiRemoteService.h"
// Call setupHandpointApiConnection once at startup (not per-adjustment)
NSString* sharedSecret = @"YOUR_SHARED_SECRET";
setupHandpointApiConnection(sharedSecret);
// Tip adjust — use transactionId from the original sale's responseFinanceStatus
// NOT eFTTransactionID — they are different fields
NSString* transaction = @"01236fc0-8192-11eb-9aca-ad4b0e95f241";
BOOL sent = tipAdjustment(transaction, 900, ^(TipAdjustmentStatus status) {
if (status == TipAdjustmentAuthorised) {
// Tip adjustment approved
} else if (status == TipAdjustmentDeclined) {
// Declined — do not retry
} else if (status == TipAdjustmentFailed) {
// System error or timeout — safe to retry once
}
});
tipAdjustment is not implemented in the Cordova plugin — calling it has no effect on either Android or iOS. Use the Cloud API or Android SDK directly.
Use instead: Cloud API or Android SDK (PAX)
Parameters
- Backoffice
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
Amount in major currency units — 9.00 = $9.00. Synchronous — result returned immediately.
| Name | Type | Required | Description |
|---|---|---|---|
{transactionID} | string (path) | Yes | transactionID from the original sale result (in the URL path) |
amount | number | Yes | Tip amount in major currency units, e.g. 9.00 for $9.00. Send 0 to void an existing tip |
taxInformation.taxAmount | number | No | Tax amount in major currency units (e.g. 1.50). Note: different format from POST /transactions, where tax is a string in minor units. 0 corrects the tax of the tip adjustment to zero |
taxInformation.taxExempt | boolean | No | Level 2 tax-exempt flag. When true, taxAmount, if sent, must be 0 |
Returns Boolean synchronously — no endOfTransaction callback fires.
| Parameter | Type | Required | Description |
|---|---|---|---|
tipAmount | BigInteger | Yes | Tip amount in minor currency units, e.g. BigInteger("900") = $9.00. BigInteger("0") to void |
currency | Currency | Yes | Currency of the original transaction, e.g. Currency.USD |
originalTransactionID | String | Yes | transactionID from the original sale result |
| Parameter | Type | Required | Description |
|---|---|---|---|
tipAmount | BigInteger | Yes | Tip amount in minor currency units, e.g. BigInteger("900") = $9.00. BigInteger("0") to void |
currency | Currency | Yes | Currency of the original transaction, e.g. Currency.USD |
originalTransactionID | String | Yes | transactionID from the original sale result |
| Parameter | Type | Required | Description |
|---|---|---|---|
transaction | NSString * | Yes | transactionId from the original sale result (note: NOT eFTTransactionID) |
tipAmount | integer | Yes | Tip amount in minor currency units, e.g. 900 = $9.00. 0 to void |
callback | block | Yes | TipAdjustmentStatus: TipAdjustmentAuthorised, TipAdjustmentDeclined, or TipAdjustmentFailed |
tipAdjustment is not implemented in the Cordova plugin — calling it has no effect on either Android or iOS. Use the Cloud API or Android SDK directly.
Use instead: Cloud API (Backoffice) or Android SDK (PAX)
Response
- Success
- Batch Closed
{
"statusMessage": "tip adjusted",
"batchNumber": "123",
"taxInformation": {
"taxAmount": 1.50,
"taxAmountIdentifier": "1"
}
}
taxInformation is only present when the gateway returns it. taxAmountIdentifier is "1" for local sales tax, "2" for tax exempt.
The batch has already been closed — tip adjustments are only available on unsettled transactions in the current open batch.
{
"error": {
"statusCode": 400,
"name": "BadRequestError",
"message": "Tip adjustment not available — batch has closed"
}
}
| Field | Type | Description |
|---|---|---|
statusMessage | string | "tip adjusted" on success |
batchNumber | string | The current open batch number, if returned by the acquirer. May be absent |
taxInformation.taxAmount | number | The tax amount recorded by the gateway, in major units. Absent when not returned |
taxInformation.taxAmountIdentifier | string | "1" local sales tax, "2" tax exempt. Absent when not returned |
Errors
| Code | Meaning | Recovery |
|---|---|---|
ORIGINAL_NOT_FOUND | Transaction ID not found | Verify the ID |
BATCH_ALREADY_CLOSED | Batch has closed | Refund the tip amount instead |
TIP_ADJUSTMENT_NOT_ENABLED | TMS config not set | Contact Handpoint onboarding team |
AMOUNT_EXCEEDS_LIMIT | Tip exceeds allowed percentage | Verify tip amount |
400 (Level 2 validation) | Invalid taxInformation, for example a non-zero taxAmount with taxExempt true, or an unsupported field | Fix the field — see Level 2 validation errors |
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-Authorization
On-device · chip, contactless, or magstripeCloud APIAndroid (PAX)CordovaBackoffice
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:
| Operation | Description |
|---|---|
| Create | Place the initial hold |
| Increase / Decrease | Adjust the held amount before capture — see the Pre-Authorization Guide |
| Capture | Finalise and charge the held amount |
| Pre-Auth Reversal | Release the hold without charging (pre-capture) |
| Capture Reversal | Cancel a completed capture (pre-settlement) — see separate section |
Implementation notes
- A pre-auth creates an authorisation hold on the cardholder's account. Funds are not captured until you send a Capture.
- The hold typically expires after 7–30 days depending on the card network and issuer. Always capture or void before expiry.
- 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. - Always reverse unused pre-authorisations — unreleased holds affect the cardholder's available credit.
PAYSAFE merchants may be onboarded on TSYS, TNS, or both. When a merchant has both processors configured, the Handpoint gateway routes based on card type: Interac cards → TNS (card-present only), all other cards → TSYS. INTERAC VOID: For Interac card transactions, show VOID in your UI — not Refund or Reverse. The only post-sale correction is VOID (full amount, card must be present at terminal). Standard refund is not available for Interac. BATCHING: Batch close applies to TSYS (non-Interac) transactions only. TNS (Interac) transactions settle independently and do not participate in the TSYS batch. Paysafe single-use token: requires merchant onboarding by Paysafe/Handpoint before use.
Create
Code
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
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"
}
hapi.preAuthorization(BigInteger("10000"), Currency.USD)
This feature is implemented in the gateway but not yet publicly released for this integration path. Contact your Handpoint integration engineer for availability.
This feature is implemented in the gateway but not yet publicly released for this integration path. Contact your Handpoint integration engineer for availability.
handpoint.preAuthorization(
{ amount: 10000, currency: handpoint.Currency.USD },
function(result) { /* store result.transactionID for capture */ },
function(error) { console.error(error); }
);
Parameters
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
| Name | Type | Required | Description |
|---|---|---|---|
operation | string | Yes | Must be "preAuthorization" |
amount | string | Yes | Authorisation amount in smallest currency unit as a string |
currency | string | Yes | ISO 4217 currency code |
terminal_type | string | Yes | Terminal model, e.g. "PAXA920" |
serial_number | string | Yes | Target terminal serial number |
transactionReference | string | No | UUID v4. Send on Pre-Auth create only — do not send on Capture, Void, or Increase. Enables status queries and groups all lifecycle operations |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Authorisation amount in minor currency units |
currency | Currency | Yes | Currency enum, e.g. Currency.USD |
options | MerchantAuthOptions? | No | Pass MerchantAuthOptions (not SaleOptions) to override merchant ID / terminal ID per acquirer for multi-MID. See Authentication |
This feature is implemented in the gateway but not yet publicly released for this integration path. Contact your Handpoint integration engineer for availability.
This feature is implemented in the gateway but not yet publicly released for this integration path. Contact your Handpoint integration engineer for availability.
| Name | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Authorisation amount in smallest currency unit |
currency | string | Yes | Currency constant, e.g. handpoint.Currency.USD |
Testing
Test pre-auth on the TEST/DEMO merchant or staging device.
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
| Scenario | How to trigger |
|---|---|
| Create hold | Send with transactionReference — verify finStatus: AUTHORISED and no amount settled yet |
| Full lifecycle | Create → Capture → query /status/all — verify both operations in chain |
| Release without capture | Create → Void — verify hold released, no settlement |
transactionReference on subsequent ops | Send transactionReference on Capture or Void — verify it is not accepted |
| Scenario | How to trigger |
|---|---|
| Create | hapi.preAuthorization() — verify AUTHORISED in endOfTransaction |
| Full lifecycle | Create → hapi.preAuthorizationCapture() — verify both results |
| Void unused hold | Create → hapi.preAuthorizationReversal(originalTransactionID) — verify AUTHORISED and no capture |
Pre-auth is not supported on HiLite — use Cloud API.
Pre-auth is not supported on iOS HiLite — use Cloud API.
| Scenario | How to trigger |
|---|---|
| Create | handpoint.preAuthorization() — verify finStatus === "AUTHORISED" |
| Full lifecycle | Create → handpoint.preAuthorizationCapture() |
For complete lifecycle test scenarios, see Testing Edge Cases.
Capture
No card required — settle the held amountCloud APIAndroid (PAX)CordovaBackoffice
Pre-Authorization Capture
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:
- The capture amount can be less than the pre-auth amount. Most acquirers also allow a small overage (check your acquirer rules).
- You must capture before the hold expires (typically 7–30 days).
- After capture, a standard refund process applies if the cardholder requests a return.
- Backoffice
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
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.
hapi.preAuthorizationCapture(
BigInteger("9500"),
Currency.USD,
"01236fc0-8192-11eb-9aca-ad4b0e95f241" // originalTransactionID from pre-auth result
)
This feature is implemented in the gateway but not yet publicly released for this integration path. Contact your Handpoint integration engineer for availability.
This feature is implemented in the gateway but not yet publicly released for this integration path. Contact your Handpoint integration engineer for availability.
handpoint.preAuthorizationCapture(
{ amount: 9500, currency: handpoint.Currency.USD, originalTransactionID: "01236fc0-8192-11eb-9aca-ad4b0e95f241" },
function(result) { /* handle */ },
function(error) { console.error(error); }
);
Parameters
- Backoffice
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
Amount in major currency units — "95.00" = $95.00. Synchronous — result returned immediately.
| Parameter | Type | Required | Description |
|---|---|---|---|
originalGuid | string | Yes | transactionID from the pre-authorization result (UUID v4) |
capturedAmount | string | Yes | Capture amount in major currency units, e.g. "95.00" for $95.00; at most 12 digits, or 13 characters with a decimal point, no leading zeros except a single 0 before the decimal point, as in 0.50 |
tipAmount | string | No | Optional tip in major currency units — Cloud API only; not supported in SDK paths |
taxInformation | object | No | Level 2 tax data: taxAmount (string, major currency units, e.g. "9.50") and taxExempt (boolean). taxAmount is required: "0" when taxExempt is true, greater than "0" otherwise, and never above capturedAmount. Must be sent with purchaseOrderNumber |
purchaseOrderNumber | string | No | Level 2 purchase order number. Must be sent with taxInformation. Alphanumeric, max 25 characters |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Capture amount in smallest currency unit, e.g. BigInteger("9500") = $95.00 |
currency | Currency | Yes | Currency of the original pre-authorization, e.g. Currency.USD |
originalTransactionID | String | Yes | transactionID from the pre-authorization result |
options | Options? | No | Optional — supports customerReference |
This feature is implemented in the gateway but not yet publicly released for this integration path. Contact your Handpoint integration engineer for availability.
This feature is implemented in the gateway but not yet publicly released for this integration path. Contact your Handpoint integration engineer for availability.
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Capture amount in smallest currency unit, e.g. 9500 = $95.00 |
currency | string | Yes | Currency constant, e.g. handpoint.Currency.USD |
originalTransactionID | string | Yes | transactionID from the pre-authorization result |
Errors
Backoffice (POST /preauthorization/capture) — synchronous, errors are immediate:
| HTTP | code | message | Meaning | What to do |
|---|---|---|---|---|
400 | 5001 | NullPointerException | originalGuid not found | Verify the GUID is the transactionID from the pre-auth create result. Code 5001 for this endpoint always means the original transaction was not found |
400 | 4066 | Partial reversal amount exceeds original amount | capturedAmount exceeds the pre-auth hold amount | Reduce the capture amount |
400 | 3051 | Already reversed | Pre-auth was already voided | Check your records — void and capture are mutually exclusive; you cannot capture after a void |
400 | 3211 | (pre-auth already settled) | Pre-auth has already been captured — a second capture is not possible | Verify state via the TXN Feed API; if the first capture succeeded, issue a refund if needed |
400 | — | Invalid capturedAmount, must be in the major unit of currency (20.50 is 20.50 EUR) | capturedAmount doesn't match the format above | Send a decimal string such as "95.00" |
400 | — | (Level 2 validation message) | Invalid taxInformation or purchaseOrderNumber, for example Invalid taxInformation.taxAmount, must not exceed capturedAmount | Fix the field — see Level 2 validation errors |
403 | — | No valid key found in header | Invalid API key | Check ApiKeyCloud header |
422 | VALIDATION_FAILED | Missing required field | Request body is missing originalGuid or capturedAmount | See details array in response for the specific field |
Testing
- Backoffice
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
| Scenario | How to trigger |
|---|---|
| Capture full hold amount | capturedAmount = original Pre-Auth amount — verify AUTHORISED |
| Capture partial amount | capturedAmount < original amount — verify settled amount in portal |
| Capture with tip | Include tipAmount — verify total settled = capturedAmount + tipAmount |
Invalid originalGuid | Use a random UUID — verify ORIGINAL_NOT_FOUND |
| Scenario | How to trigger |
|---|---|
| Capture | hapi.preAuthorizationCapture(amount, currency, originalTransactionID) |
| Partial capture | Pass captureAmount less than original hold |
Not supported on HiLite.
Not supported on iOS HiLite.
| Scenario | How to trigger |
|---|---|
| Capture | handpoint.preAuthorizationCapture() with originalTransactionID |
Pre-Auth Reversal
Release the hold without chargingCloud APIAndroid (PAX)CordovaBackoffice
Pre-Auth Reversal
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.
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.
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
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"
}
hapi.preAuthorizationReversal("01236fc0-8192-11eb-9aca-ad4b0e95f241")
This feature is implemented in the gateway but not yet publicly released for this integration path. Contact your Handpoint integration engineer for availability.
This feature is implemented in the gateway but not yet publicly released for this integration path. Contact your Handpoint integration engineer for availability.
handpoint.preAuthorizationReversal(
{ originalTransactionID: "01236fc0-8192-11eb-9aca-ad4b0e95f241" },
function(result) { /* handle */ },
function(error) { console.error(error); }
);
Parameters
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
| Parameter | Type | Required | Description |
|---|---|---|---|
operation | string | Yes | Must be "preAuthorizationReversal" |
terminal_type | string | Yes | Terminal model, e.g. "PAXA920" |
serial_number | string | Yes | Target terminal serial number |
originalTransactionId | string | Yes | transactionID from the pre-authorization result |
| Parameter | Type | Required | Description |
|---|---|---|---|
originalTransactionID | String | Yes | transactionID from the pre-authorization result |
This feature is implemented in the gateway but not yet publicly released for this integration path. Contact your Handpoint integration engineer for availability.
This feature is implemented in the gateway but not yet publicly released for this integration path. Contact your Handpoint integration engineer for availability.
| Parameter | Type | Required | Description |
|---|---|---|---|
originalTransactionID | string | Yes | transactionID from the pre-authorization result |
Errors
Void is a with-reader operation — errors arrive in the polled result, not in the initial 202 response.
finStatus | Meaning | What to do |
|---|---|---|
DECLINED / FAILED | originalTransactionId not found | Verify the GUID is the transactionID from the pre-auth create result |
DECLINED | Pre-auth has already been captured or voided — hold no longer exists | Check transaction state before voiding |
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
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
| Scenario | How to trigger |
|---|---|
| Void before capture | Create Pre-Auth → send Void — verify AUTHORISED and no settlement in portal |
| Void after capture | Create → Capture → Void — verify whether acquirer supports post-capture void |
| Invalid reference | Use a random originalTransactionId — verify ORIGINAL_NOT_FOUND |
| Scenario | How to trigger |
|---|---|
| Void | hapi.preAuthorizationReversal(originalTransactionID) before capture |
Not supported on HiLite.
Not supported on iOS HiLite.
| Scenario | How to trigger |
|---|---|
| Void | handpoint.preAuthorizationReversal({ originalTransactionID: "..." }) before capture |
Tokenization
Paysafe Single-Use Token
Paysafe · single-use token — for use on Paysafe APIs; consult Paysafe teamCloud APIAndroid (PAX)Android (HiLite)iOS (HiLite)CordovaJavaScript SDKWindows (.NET)
What this does
Reads the card at the terminal and generates a Paysafe single-use token — a short-lived, one-time credential that lets you securely pass card data to Paysafe's Customer Vault without your server ever handling the raw PAN. Handpoint Gateway returns the token in the cardToken field of the transaction result.
When to use it
Use when your integration also calls Paysafe's own Customer Vault API to store a card on file. The Handpoint-issued single-use token is the secure input to that vault call — once consumed, Paysafe returns a permanent paymentToken your system can store for recurring charges.
This token is intended for use on Paysafe's Customer Vault API, not Handpoint's /moto/sale endpoint. The Handpoint Gateway does not detokenize Paysafe single-use tokens for MOTO processing.
Contact the Paysafe team before building a token-based flow to confirm the token format Handpoint returns meets your specific integration requirements.
How the vaulting flow works
Terminal reads card
→ Handpoint Gateway requests single-use token from Paysafe
→ Paysafe returns single-use token (expires in ~15 min)
→ Handpoint returns cardToken to your system
Your system calls:
POST https://api.paysafe.com/customervault/v1/profiles
{ "singleUseToken": "<cardToken>" }
→ Paysafe vaults the card and returns a permanent paymentToken
→ Your system stores the permanent paymentToken for future charges
The permanent paymentToken is what Paysafe intends for recurring billing and is reusable with no expiry.
Implementation notes
- 15-minute expiry. The single-use token expires ~15 minutes after generation (
timeToLiveSeconds: 899in Paysafe's response). Pass it to the Paysafe Customer Vault API within that window. - Non-deterministic. The same card submitted twice produces different tokens each time — these tokens are not stable card identifiers and cannot be used for loyalty or card-matching.
- Not for MOTO. Handpoint does not detokenize Paysafe single-use tokens. You cannot pass
cardTokento/moto/saleon a Paysafe merchant — use the permanent vault token through Paysafe's own APIs instead. - Token scope: Tokens are specific to the Paysafe merchant configuration. Test tokens (from
api.test.paysafe.com) are not valid in production. - Consult Paysafe. Work with the Paysafe team to confirm your merchant's Customer Vault setup and that the token format Handpoint returns is compatible with your integration before going live.
The Paysafe vaulting flow described above is based on Paysafe's public Customer Vault API documentation. Please have the Handpoint team verify this matches the actual gateway implementation before publishing.
Code
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows (.NET)
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"
}
The cardToken in the response is a Paysafe single-use token for use on Paysafe APIs.
// Tokenize only
hapi.tokenizeCard()
// Sale and tokenize
val options = SaleAndTokenizeOptions()
hapi.sale(BigInteger("1000"), Currency.USD, options)
override fun endOfTransaction(result: TransactionResult, device: Device) {
val token = result.cardToken // Paysafe single-use token
val provider = result.cardTokenProvider // "PAYSAFE"
}
hapi.tokenizeCard()
override fun endOfTransaction(result: TransactionResult, device: Device) {
val token = result.cardToken
}
heftClient.tokenizeCard()
// Token returned in result.cardToken
handpoint.tokenizeCard(
{},
function(result) { const token = result.cardToken; },
function(error) { console.error(error); }
);
// Tokenize only
const { transactionResult } = hp.tokenizeCard('USD', { terminalType: 'PAXA920', serialNumber: '082104578' });
const result = await transactionResult;
const token = result.cardToken; // Paysafe single-use token
// Sale and tokenize
const { transactionResult: tr } = hp.saleAndTokenizeCard(1000, 'USD', { terminalType: 'PAXA920', serialNumber: '082104578' });
const saleResult = await tr;
const token = saleResult.cardToken; // Paysafe single-use token
// Tokenize only
var op = hapi.TokenizeCard(Currency.USD);
// Sale and tokenize
var options = new SaleAndTokenizeOptions();
var op = hapi.Sale(new BigInteger(1000), Currency.USD, options);
// Result delivered via EndOfTransaction callback
public void EndOfTransaction(TransactionResult result, Device device)
{
string token = result.CardToken; // Paysafe single-use token
string provider = result.CardTokenProvider; // "PAYSAFE"
}
Response fields
| Field | Description |
|---|---|
cardToken | Paysafe single-use token — pass to Paysafe APIs; not for Handpoint MOTO |
cardTokenProvider | "PAYSAFE" |
expiryDateMMYY | Token expiry (matches card expiry) |
Testing
Test tokens are returned on the TEST/DEMO merchant or staging device. Confirm with Paysafe that test tokens are accepted on the Paysafe API sandbox before running end-to-end tests.
TokenEx
3rd-party · TokenEx — loyalty / card-matching; no detokenization through HandpointCloud APIAndroid (PAX)Android (HiLite)iOS (HiLite)CordovaJavaScript SDKWindows (.NET)
What this does
Reads the card at the terminal and stores it in the TokenEx token vault — a 3rd-party PCI-compliant token service. Handpoint returns a TokenEx token in the cardToken field. The token represents the card identity without storing the PAN in your system.
When to use it
Use for loyalty programs and card-matching flows — identifying that the same card is being used across multiple visits or transactions. TokenEx tokens let you correlate card-present transactions to a loyalty account or stored profile without ever handling PAN data.
TokenEx tokens cannot be detokenized through Handpoint. They cannot be used for back-office MOTO charges or remote sales. If you need card-not-present charging from a stored token, see the Cygma flavor (EPI acquirer) or Paysafe Single-Use Token flavor (Paysafe acquirer).
Implementation notes
- 3rd-party vault: TokenEx operates independently of Handpoint acquirers. The same TokenEx token can be issued regardless of which Handpoint acquirer processes the sale.
- No detokenization: The original PAN cannot be retrieved from a TokenEx token through Handpoint's APIs. TokenEx supports detokenization through its own APIs for clients with a direct TokenEx contract — this is outside the Handpoint integration.
- Token stability: The same card consistently maps to the same TokenEx token (within the same merchant configuration), making it reliable for card-matching and loyalty purposes.
- Token scope: Tokens are specific to the merchant configuration. Test tokens are not valid in production.
Code
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows (.NET)
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"
}
The cardToken in the response is a TokenEx token. Match it to a loyalty profile or record it for card-correlation purposes.
// Tokenize only
hapi.tokenizeCard()
// Sale and tokenize
val options = SaleAndTokenizeOptions()
hapi.sale(BigInteger("1000"), Currency.USD, options)
override fun endOfTransaction(result: TransactionResult, device: Device) {
val token = result.cardToken // TokenEx token — store for loyalty/matching
val provider = result.cardTokenProvider // "TOKENEX"
}
hapi.tokenizeCard()
override fun endOfTransaction(result: TransactionResult, device: Device) {
val token = result.cardToken
}
heftClient.tokenizeCard()
// Token returned in result.cardToken
handpoint.tokenizeCard(
{},
function(result) { const token = result.cardToken; },
function(error) { console.error(error); }
);
// Tokenize only
const { transactionResult } = hp.tokenizeCard('USD', { terminalType: 'PAXA920', serialNumber: '082104578' });
const result = await transactionResult;
const token = result.cardToken; // TokenEx token — store for loyalty/card-matching
// Sale and tokenize
const { transactionResult: tr } = hp.saleAndTokenizeCard(1000, 'USD', { terminalType: 'PAXA920', serialNumber: '082104578' });
const saleResult = await tr;
const token = saleResult.cardToken; // TokenEx token
// Tokenize only
var op = hapi.TokenizeCard(Currency.USD);
// Sale and tokenize
var options = new SaleAndTokenizeOptions();
var op = hapi.Sale(new BigInteger(1000), Currency.USD, options);
// Result delivered via EndOfTransaction callback
public void EndOfTransaction(TransactionResult result, Device device)
{
string token = result.CardToken; // TokenEx token — store for loyalty/card-matching
string provider = result.CardTokenProvider; // "TOKENEX"
}
Response fields
| Field | Description |
|---|---|
cardToken | TokenEx token — stable card identifier for loyalty / card-matching. Cannot be used for MOTO |
cardTokenProvider | "TOKENEX" |
expiryDateMMYY | Token expiry (matches card expiry) |
Testing
Test tokens are returned on the TEST/DEMO merchant or staging device. Use the TokenEx sandbox to verify card-matching behavior before testing on production.
Void
Cancel a transaction before settlement — card must be presentCloud APIAndroid (PAX)Android (HiLite)iOS (HiLite)Cordova
Void applies exclusively to Interac card transactions. For non-Interac cards, use Reversal instead — the Interac network does not support standard reversal.
What this does
Voids a transaction — used for Interac card transactions where standard reversal is not available.
When to use it
Use specifically for Interac card transactions via TNS. For non-Interac transactions, use Reversal instead. The card must be physically present at the terminal. This must occur before settlement.
Implementation notes
- Interac / TNS only. For non-Interac cards, use Reversal instead.
- The card must be inserted or tapped at the terminal — card-not-present void is not supported by the Interac network.
- Full amount only — partial voids are not supported.
- ISV UI note: label this button "VOID" in your application — never "Refund" or "Reverse". This is an Interac network requirement.
- SDK behaviour: the Android SDK method called is
refund(). The Handpoint gateway detects that the original transaction was processed via TNS (Interac) and automatically maps the refund request to a TNS VOID internally. You do not call a separate void function.
See also: Interac VOID — implementation guide
PAYSAFE merchants may be onboarded on TSYS, TNS, or both. When a merchant has both processors configured, the Handpoint gateway routes based on card type: Interac cards → TNS (card-present only), all other cards → TSYS. INTERAC VOID: For Interac card transactions, show VOID in your UI — not Refund or Reverse. The only post-sale correction is VOID (full amount, card must be present at terminal). Standard refund is not available for Interac. BATCHING: Batch close applies to TSYS (non-Interac) transactions only. TNS (Interac) transactions settle independently and do not participate in the TSYS batch. Paysafe single-use token: requires merchant onboarding by Paysafe/Handpoint before use.
Code
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json
{
"operation": "refund",
"amount": "1000",
"currency": "CAD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"originalTransactionId": "01236fc0-8192-11eb-9aca-ad4b0e95f241"
}
The gateway detects that the original transaction was Interac/TNS and maps to the correct void protocol automatically. Do not include a transactionReference — this is a subsequent operation.
// Call refund() — the gateway detects the original was an Interac/TNS
// transaction and maps this to a TNS VOID automatically.
// Show "VOID" in your UI, not "Refund".
hapi.refund(
BigInteger("1000"), // full original amount
Currency.CAD,
"01236fc0-8192-11eb-9aca-ad4b0e95f241" // originalTransactionID from sale result
)
// Same as Android PAX — call refund(), gateway maps to TNS VOID.
hapi.refund(
BigInteger("1000"),
Currency.CAD,
"01236fc0-8192-11eb-9aca-ad4b0e95f241"
)
// Call refundWithAmount() — gateway detects the original was Interac/TNS and maps to TNS VOID.
// Card must be present at terminal. Show "VOID" in your UI.
heftClient.refundWithAmount(1000, currency: "CAD", transaction: "01236fc0-8192-11eb-9aca-ad4b0e95f241")
// Call saleReversal — gateway maps to TNS VOID for Interac transactions.
handpoint.saleReversal(
{
amount: 1000,
currency: "CAD",
originalTransactionID: "01236fc0-8192-11eb-9aca-ad4b0e95f241"
},
function(result) { /* handle */ },
function(error) { console.error(error); }
);
Parameters
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
| Name | Type | Required | Description |
|---|---|---|---|
operation | string | Yes | Must be "refund" — gateway maps to TNS VOID automatically |
amount | string | Yes | Full original sale amount in smallest currency unit as a string |
currency | string | Yes | Must be "CAD" for Interac |
terminal_type | string | Yes | Terminal model, e.g. "PAXA920" |
serial_number | string | Yes | Target terminal serial number |
originalTransactionId | string | Yes | transactionID from the original Interac sale result |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Full original sale amount in smallest currency unit |
currency | Currency | Yes | Must be Currency.CAD for Interac |
originalTransactionID | String | Yes | transactionID from the original Interac sale result |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | BigInteger | Yes | Full original sale amount in smallest currency unit |
currency | Currency | Yes | Must be Currency.CAD for Interac |
originalTransactionID | String | Yes | transactionID from the original Interac sale result |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | NSInteger | Yes | Full original sale amount in smallest currency unit |
currency | NSString * | Yes | Must be @"CAD" for Interac |
transaction | NSString * | Yes | transactionID from the original Interac sale result |
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Full original sale amount in smallest currency unit |
currency | string | Yes | Must be "CAD" for Interac |
originalTransactionID | string | Yes | transactionID from the original Interac sale result |
Errors
| Code | Meaning | Recovery |
|---|---|---|
CARD_NOT_PRESENT | Card was not inserted/tapped | Ask cardholder to present card at terminal |
ALREADY_SETTLED | Transaction already settled | Credit refund is not available for Interac |
ORIGINAL_NOT_FOUND | Reference not found | Verify reference |
Testing
Test void (Interac/TNS) on the TEST/DEMO merchant. A physical card must be present at the terminal — card must be inserted or tapped.
Interac Card Transactions
Interac card transactions — full void only, card present requiredCloud APIAndroid (PAX)JavaScript SDKWindows (.NET)
If you are building for merchants that process Interac card transactions, this section is your integration reference. Interac cards are routed to the TNS processor automatically — no special configuration is required by the ISV beyond what is already set up for Paysafe.
ISV onboarding checklist:
- Your
Saleflow already covers Interac EMV sales. - You must implement EMV VOID (not Refund) as the post-sale correction for Interac transactions — see Void section above.
- Remote reversal (
POST /reversal) is not available for Interac — card must be physically present at the terminal for any post-sale correction.
Supported operations for Interac cards
| Operation | Support | Notes |
|---|---|---|
| EMV Sale | ✅ | All integration paths — see Sale section |
| Sale with Tip | ✅ | Tip selection screen on terminal; tip is included in the authorized total (F4). No separate tip breakdown field is forwarded to TNS. Confirmed in production. |
| EMV VOID | ✅ | Same-day, before settlement cut-off, card must be present — see Void section above |
| Remote Reversal | ❌ | Not available — card must be present at terminal; no back-office void path |
| MOTO / Remote Sale | ❌ | Interac network does not support card-not-present |
| Credit Refund | ❌ | Not available for Interac — use VOID (same-day) instead |
| Pre-Authorization | ❌ | Explicitly rejected by Interac network |
| Tip Adjustment | ❌ | Hard-blocked at the TNS protocol validator — post-sale tip modification is not possible on this route |
| Key Entry Sale | ❌ | Not supported on Interac |
For code, parameters, and testing, see the Void section above — it covers all integration paths including Cordova.