EmerchantPay
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows SDK
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
| Name | Type | Required | Description |
|---|---|---|---|
operation | string | Yes (Cloud API) | "sale" |
amount | string / BigInteger | Yes | Smallest currency unit — "1000" = $10.00 |
currency | string | Yes | ISO 4217 code, e.g. "USD", "EUR", "CAD" |
serial_number | string | Yes (Cloud API) | Target terminal serial number |
terminal_type | string | Yes (Cloud API) | Terminal model, e.g. "PAXA920" |
transactionReference | string | No (Cloud API / JS SDK) | 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 (Cloud API) | URL to receive the result via POST; if omitted, poll GET /transaction-result/{id} |
Android SDK SaleOptions fields (optional third argument to hapi.sale()):
| Field | Type | Description |
|---|---|---|
customerReference | String? | Merchant reference echoed in TransactionResult |
tipConfiguration | TipConfiguration? | Pre-configure tip prompt on the terminal |
pinBypass | Boolean | Offer PIN bypass where acquirer-supported |
checkDuplicates | Boolean | Enable duplicate detection on the gateway |
merchantAuth | MerchantAuth? | Override MID/TID for multi-MID merchants |
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
| Name | Type | Required | Description |
|---|---|---|---|
operation | string | Yes (Cloud API) | "moToSale" — triggers card entry screen on terminal |
amount | string | Yes | Smallest currency unit — "1000" = $10.00 |
currency | string | Yes | ISO 4217 code |
serial_number | string | Yes (Cloud API) | PAX terminal serial number |
terminal_type | string | Yes (Cloud API) | Terminal model, e.g. "PAXA920" |
transactionReference | string | No | UUID v4 for idempotency and status queries |
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 (POST /moto/sale):
| Name | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | Amount in major currency units — "10.00" = $10.00 (note: different from with-reader which uses minor units) |
currency | string | Yes | ISO 4217 code |
cardToken | string | Yes | Token from a supported provider — never a raw PAN |
transactionReference | string | Recommended | UUID v4. Send on every original transaction — required to recover the outcome via GET /transactions/{reference}/status/all when no result is received or when finStatus: UNDEFINED is returned. See Transaction Recovery. |
customerReference | string | No | Merchant reference forwarded to the acquirer |
Android SDK MoToOptions fields:
| Field | Type | Description |
|---|---|---|
cardToken | String? | Card token for back-office MOTO — omit to show terminal entry screen instead |
channel | MoToChannel? | MAIL_ORDER or TELEPHONE_ORDER |
customerReference | String? | Merchant reference echoed in TransactionResult |
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 |
Sale with Tokenization
On-device · stores card token for future chargesCloud APIAndroid (PAX)Android (HiLite)iOS (HiLite)CordovaJavaScript SDKWindows (.NET)
Store a reusable card token during the sale so future card-not-present charges can be made without the cardholder being present again. The token is returned in the TransactionResult and can be used for subsequent back-office MOTO sales.
When to use it
Use when onboarding a new customer in person — take the first payment as a normal card-present sale and simultaneously capture a card token for future recurring charges, subscriptions, or card-on-file billing.
Code
- Cloud API
- Android (PAX)
- Android (HiLite)
- iOS (HiLite)
- Cordova
- JavaScript SDK
- Windows (.NET)
Tokenization happens as a separate operation via the /transactions endpoint with "operation": "tokenizeCard". The resulting token can then be used in back-office MOTO sales.
POST https://cloud.handpoint.com/transactions
ApiKeyCloud: YOUR_MERCHANT_API_KEY
Content-Type: application/json
{
"operation": "tokenizeCard",
"currency": "USD",
"terminal_type": "PAXA920",
"serial_number": "082104578",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f"
}
The token is returned in the TransactionResult.cardToken field. Store it and use it in subsequent POST /moto/sale requests.
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 // non-null only when SaleAndTokenizeOptions was passed
// store token for future back-office sales
}
}
hapi.tokenizeCard()
override fun endOfTransaction(result: TransactionResult, device: Device) {
if (result.finStatus == FinancialStatus.AUTHORISED) {
val token = result.cardToken
}
}
heftClient.tokenizeCard()
func responseFinanceStatus(_ info: (any FinanceResponseInfo)!) {
if info.finStatus() == "AUTHORISED" {
// store info.eFTTransactionID() for potential reversal
}
}
handpoint.tokenizeCard(
{ currency: handpoint.Currency.USD },
function(result) {
if (result.finStatus === "AUTHORISED") {
const token = result.cardToken; // store securely
}
},
function(error) { console.error(error); }
);
// @handpoint/cloud-js-sdk — tokenize without sale
const { transactionResult } = hp.tokenizeCard(
'USD',
{ terminalType: 'PAXA920', serialNumber: '082104578' }
);
const result = await transactionResult;
if (result.finStatus === 'AUTHORISED') {
const token = result.cardToken; // store securely for future MOTO charges
}
var op = hapi.TokenizeCard(Currency.USD);
public void EndOfTransaction(TransactionResult result, Device device)
{
if (result.FinStatus == FinancialStatus.AUTHORISED)
{
string token = result.CardToken; // store securely for future MOTO charges
}
}
Result fields
| Field | Description |
|---|---|
cardToken | Opaque token string issued by the acquirer's token provider. Store this — do not log or expose it. |
finStatus | AUTHORISED on success |
Using the token for future charges
Pass cardToken in the Remote Sale (MOTO) flavor's cardToken field for subsequent card-not-present charges.
Tokens are specific to the merchant and acquirer configuration. A token issued on a test/staging merchant cannot be used on production, and vice versa. Token validity periods vary by acquirer — consult your acquirer documentation.
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.