Handpoint API
Three Handpoint surfaces consolidated here: the REST API for taking payments on physical terminals (cloud.handpoint.com), the TMS API for provisioning merchants and terminals (tmsdoc.handpoint.com), and the Transaction Feed API for pulling settled transactions into your reporting (txnfeed.handpoint.io). Each surface has its own credential – they don’t cross over.
Reformatted from the public Handpoint docs. Where these docs and Handpoint’s upstream disagree, trust upstream.
Introduction
Handpoint is a P2PE-encrypted terminal cloud. Merchant software hits our REST API to tell a terminal to take a sale; the terminal runs the EMV / contactless / swipe flow and returns the result.
Three independent APIs ship under the Handpoint umbrella, each with its own auth and its own base URL:
- REST API – taking payments on physical terminals.
- TMS API – onboarding merchants, assigning terminals, minting API keys.
- Transaction Feed API – pulling transactions for reporting / dashboards.
Merchant360 supports card-present and card-not-present payments in one API. Card-present sales go to the Handpoint reader through Merchant360’s REST API as a semi-integrated flow: your software sends the amount, the reader takes the card, and you get the result as the same payment object a keyed charge returns. You don’t need a Handpoint API key, host, device type or serial number. Card-present: https://www.cygma.cloud/merchant360#terminal. Every transaction for reporting, all tenders: https://www.cygma.cloud/merchant360#transactions. This page documents Handpoint’s native APIs for direct Handpoint integrations.
Integration paths and hosts
Two ways to drive a Handpoint terminal directly:
- Cloud API (REST). Your server POSTs to a PAX terminal in Integrated (cloud) mode, then polls for the result or receives a callback. Auth:
ApiKeyCloudheader with a merchant Cloud key. A merchant can have several keys. - SDKs. Android SDK on PAX, Android / iOS for HiLite over Bluetooth, JavaScript SDK (through Cloud), Windows .NET, Cordova / Ionic. SDKs use the merchant’s SSK (one per merchant;
GET /devicesreturns it).
| Purpose | Production | Staging (debug devices) | Auth header |
|---|---|---|---|
| Cloud API | cloud.handpoint.com | cloud.handpoint.io | ApiKeyCloud: <key> |
| Status / recovery | transactions.handpoint.com | transactions.handpoint.io | ApiKeyCloud: <key> |
| Transaction feed | txnfeed.handpoint.com | txnfeed.handpoint.io | Authorization: <KEY> |
| TMS | tms.handpoint.com | Authorization: <PARTNER_KEY> |
- The
.iohosts serve debug / staging devices and DEMO merchants only. Credentials do not work across.ioand.com. /statuslives only ontransactions.*; it returns 404 oncloud.*.- Amounts. Cloud and SDK operations take minor units as strings (
"1000"= 10.00). Back-office endpoints (/reversal,/moto/*,/preauthorization/*, tip adjustment) take major units ("95.00"). - Device names are
serialNumber-terminalType.
Authentication
Each surface uses a different credential and a different header:
- REST API –
ApiKeyCloudheader. The key is unique per merchant; you mint it through the TMS API. - TMS API – JWT obtained from a Login endpoint, passed in
Authorization: Beareron subsequent calls. - Transaction Feed API –
AnalyticsApiKeyheader. Separate key from the REST API.
# REST API (transactions on terminals) ApiKeyCloud: <merchant-api-key> # TMS API (merchant + terminal provisioning) Authorization: Bearer <jwt-from-login> # Transaction Feed API (reporting) AnalyticsApiKey: <analytics-api-key>
Environments
Pick the base URL by terminal type, not by where your code is running. Production / demo terminals hit cloud.handpoint.com; debug terminals hit cloud.handpoint.io. Demo terminals use a mock acquirer – no funds move, but every other behavior is real.
The .io hosts serve debug devices and DEMO merchants only. Credentials minted on one side do not work on the other. See Integration paths & hosts.
# Production (REST) – also used for "demo" terminals (mock acquirer, no funds moved) https://cloud.handpoint.com # Debug / development (REST) https://cloud.handpoint.io # Transaction status / lookup (REST) https://transactions.handpoint.com # production https://transactions.handpoint.io # debug # Transaction Feed https://txnfeed.handpoint.com # production https://txnfeed.handpoint.io # debug
Flow models (push vs poll)
Every transaction request returns 202 Accepted with a transactionResultId – the terminal hasn’t actually run the transaction yet. You get the real result one of two ways:
- Push (4.1) – supply
callbackUrl+tokenin the request. Handpoint POSTs the result to your URL when the terminal finishes; the token echoes inAUTH-TOKENso you can authenticate the callback. Your callback URL must accept a certificate authority Android 5-10 supports (terminal OS versions). - Poll (4.2) – omit those fields.
GET /transaction-result/{id}with the returned id. Returns204while in flight,200with the result when done.
// Push (4.1) – include callbackUrl + token in the
// transaction request. Handpoint POSTs the result back to
// your callback when the terminal finishes; AUTH-TOKEN
// echoes the token you sent.
{
"operation": "sale",
"amount": "10000",
"currency": "EUR",
"callbackUrl": "https://your.app/handpoint-callback",
"token": "session-7c1d"
}
// Poll (4.2) — omit callbackUrl + token. The API returns
// transactionResultId immediately; you poll
// GET /transaction-result/{id} until you get a 200.
{
"operation": "sale",
"amount": "10000",
"currency": "EUR"
}Callback URL
Send callbackUrl and token on the POST. When the terminal finishes, Handpoint POSTs the TransactionResult to your URL with the token in the auth-token header (it may arrive as AUTH-TOKEN; compare case-insensitively).
- Use a fresh UUID as the
tokenfor every transaction and reject callbacks whose header does not match. - Retries: every 5 s for 100 s, then exponential backoff capped at 15 min, for up to 2 days. Any 2xx stops them.
recoveredTransaction: truemarks a redelivery. Dedupe ontransactionReference.- Return 200 even for
UNDEFINED(keep resolving it through recovery). - The callback host’s TLS certificate must chain to a common CA (DigiCert, GlobalSign, Sectigo); terminal OS trust stores are limited.
curl -X POST https://cloud.handpoint.com/transactions \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"operation": "sale",
"amount": "1800",
"currency": "USD",
"terminal_type": "PAXA920MAX",
"serial_number": "2740199794",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f",
"callbackUrl": "https://pos.example.com/handpoint/callback",
"token": "6f2c0f4e-9a51-4c4b-8f0e-2d7b1f0c9e11"
}'POST /handpoint/callback HTTP/1.1 Host: pos.example.com Content-Type: application/json auth-token: 6f2c0f4e-9a51-4c4b-8f0e-2d7b1f0c9e11
REST API
Taking payments on physical terminals – sale, refund, void, pre-auth, MOTO, batch, and device controls.
POST /payments with terminal_id sends the sale to the merchant’s Handpoint reader (semi-integrated) with your Merchant360 key – the same endpoint, payment object and webhooks as card-not-present. Documentation: https://www.cygma.cloud/merchant360#terminal
List devices
Retrieves every payment terminal (and Virtual Terminal) associated with the merchant. Use the returned serial_number + terminal_type on every subsequent transaction request.
curl -X GET https://cloud.handpoint.com/devices \ -H "ApiKeyCloud: <merchant-api-key>"
Send transaction
Sends an operation to the terminal. Valid operation values include sale, refund, refundReversal, preAuthorizationCapture, and more.
amount is in minor units (cents) as a string: "10000" = €100.00. transactionReference must be a UUID v4 for original operations; linked operations (refund, reversal) pass originalTransactionId instead.
Common error codes: 1001 device busy, 1002 device offline / not responding, 1003 cancel operation not allowed.
| Field | Type | Description |
|---|---|---|
operationrequired | string | Terminal operation: sale, refund, refundReversal, preAuthorizationCapture, etc. |
amountrequired | string | Amount in minor units (cents) as a string. "10000" = €100.00. |
currencyrequired | string | ISO 4217 currency code, e.g. EUR. |
terminal_typerequired | string | Terminal model from List devices, e.g. PAXA920. |
serial_numberrequired | string | Terminal serial number from List devices. |
transactionReferencerequired | string | UUID v4 for original operations; echoed back on the result. |
customerReferenceoptional | string | Your own reference for the operation. |
originalTransactionIdoptional | string | Links a refund or reversal to the original transaction. |
curl -X POST https://cloud.handpoint.com/transactions \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"operation": "sale",
"amount": "10000",
"currency": "EUR",
"terminal_type": "PAXA920",
"serial_number": "1547854757",
"customerReference": "op15248",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f"
}'Fetch result (poll)
Polling endpoint for flow model 4.2. Hit it until you get a 200. Returns the full transaction-result object with masked PAN, card scheme, AVS / CVV results, signature URL, customer + merchant receipt text, and the cardToken you can re-use for MOTO calls within 12 months.
| Field | Type | Description |
|---|---|---|
idrequired | string | The transactionResultId returned by POST /transactions. |
curl -X GET https://cloud.handpoint.com/transaction-result/1850025030-1776238708721 \ -H "ApiKeyCloud: <merchant-api-key>"
Transaction status
Query by the transactionReference you supplied on the original request. Useful when you’ve lost the transactionResultId or when you need to find every linked operation (refunds, reversals, captures) tied to one original sale – pass selector=all.
Lives on a different host: transactions.handpoint.com / .io.
finStatus values: IN_PROGRESS, UNDEFINED, PARTIAL_APPROVAL, AUTHORISED, DECLINED, CANCELLED, FAILED, PROCESSED, REFUNDED, CAPTURED. Which are final, timing, and retry rules: Transaction recovery.
| Field | Type | Description |
|---|---|---|
transactionReferencerequired | string | Path – the reference you supplied on the original request. |
selectoroptional | string | Path segment after /status/: first (default when omitted), last, all, or a numeric index n. |
curl -X GET https://transactions.handpoint.com/transactions/{transactionReference}/status \
-H "ApiKeyCloud: <merchant-api-key>"Transaction recovery
Persist a UUID v4 transactionReference before you send, then resolve the outcome by result id (terminal channel) or by reference (status host). GET /transactions/{ref}/status returns the first operation for the reference; /status/{all|first|last|n} selects others. Each entry carries type, finStatus, totalAmount, transactionID, and transactionReference or originalEFTTransactionID. Net = AUTHORISED sales minus refunds and reversals; a later operation’s finStatus reflects its state when it was processed.
| finStatus | Final | Meaning / action |
|---|---|---|
| IN_PROGRESS | No | Keep polling. |
| UNDEFINED | No | Keep polling until timeout. If the terminal never reached the gateway, the card was not charged. |
| PARTIAL_APPROVAL | No | US: the terminal is prompting. |
| AUTHORISED | Yes | Charged. Do not retry. |
| DECLINED | Yes | Not charged. Retry allowed (new reference). |
| CANCELLED | Yes | Not charged. Retry allowed (new reference). |
| FAILED | Yes | Not charged. Retry allowed (new reference). |
| PROCESSED | Yes | Tokenize or MOTO operation done. |
| REFUNDED | Yes | |
| CAPTURED | Yes |
Timing
- Poll
GET /transaction-result/{id}every 3 to 5 s (204in progress,200result,404unknown). Card-present typically takes 10 to 30 s. - Worst case is about 340 s. Use an active-recovery timeout of 360 s from the send, then query
/statusonce more. StillIN_PROGRESSorUNDEFINED: flag for manual reconciliation. - Never auto-retry with the same reference. A retry is a new operation with a new UUID, and only after a final non-charged status.
Two-thread variant
- Thread 1 polls the terminal channel every 5 s (authoritative). Thread 2 polls
/statusevery 10 s. - If
/statussays AUTHORISED and no terminal result has arrived, sendpingDevice(expectPING_PONG). - After 300 s
/statusis provisional. If it says AUTHORISED and no terminal result ever came, reverse the sale.
Reference rules
- Send
transactionReferenceonly on originals:sale, unlinkedrefund,saleAndTokenizeCard,preAuthorization. Not on reversal, linked refund, capture / void or tip adjust. customerReferenceis an echo only. Duplicate protection isduplicate_check(default true).- KI-2026-008 (CUS-837):
moToSaleignorestransactionReference. Poll bytransactionResultIdand storetransactionID.
Terminal-initiated reversals
The terminal can reverse an approved sale itself. The result comes back finStatus: CANCELLED with customFields.messageReasonCode 4000 plus a cause code in another custom field (Handpoint does not document that field’s name; match on the code values below). Do not retry and do not send a reversal.
| Cause | Meaning |
|---|---|
| 4021 | Timeout after approval |
| 4351 | Chip card removed early |
| 4352 | Chip declined after host approval |
| 4353 | Signature not provided in time |
| 4354 | Signature declined or partial approval rejected |
| 4580 | Processing error |
| 4002 | Suspected terminal malfunction |
curl -X GET https://transactions.handpoint.com/transactions/2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f/status \ -H "ApiKeyCloud: <merchant-api-key>"
curl -X GET https://transactions.handpoint.com/transactions/2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f/status/all \ -H "ApiKeyCloud: <merchant-api-key>"
curl -X POST https://cloud.handpoint.com/transactions \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"operation": "pingDevice",
"terminal_type": "PAXA920MAX",
"serial_number": "2740199794"
}'{
"finStatus": "CANCELLED",
"statusMessage": "Cancelled",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f",
"customFields": {
"messageReasonCode": "4000",
"<cause field>": "4351"
}
}Tip adjustment
Adjust the tip amount on an already-authorized sale before batch settlement. US restaurants only; TSYS / VANTIV acquirers only. amount is in major units (dollars) – note the mismatch with the transaction amount field which is in minor units.
EPI (TSYS): send it before the EPI batch auto-close (about 11pm EST); 0 voids the tip. Errors ORIGINAL_NOT_FOUND, BATCH_ALREADY_CLOSED, TIP_ADJUSTMENT_NOT_ENABLED, AMOUNT_EXCEEDS_LIMIT. See EPI acquirer features.
| Field | Type | Description |
|---|---|---|
guidrequired | string | Path – GUID of the authorized sale to adjust. |
amountrequired | string | Body – new tip amount in major units (dollars), e.g. "5.25". |
curl -X POST https://cloud.handpoint.com/transactions/ff6da784-8b57-11ed-9891-ebe2a88ff071/tip-adjustment \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"amount": "5.25"
}'Card token (deferred tokenization)
Retrieve a card token from a completed card-present transaction so you can charge the same card via the MOTO endpoints later. Eligible transaction types: sale, refund, preAuthorizationCapture, moToSale, moToRefund. Must be requested within 12 months of the original transaction.
| Field | Type | Description |
|---|---|---|
guidrequired | string | GUID of the completed transaction to tokenize. |
curl -X GET https://cloud.handpoint.com/transactions/{guid}/token \
-H "ApiKeyCloud: <merchant-api-key>"Pre-auth increase / decrease
Modify the authorized amount on an open pre-authorization without re-running the card. increaseAmount is the delta in major units. Pass subtract: "1" to decrease.
| Field | Type | Description |
|---|---|---|
originalGuidrequired | string | GUID of the open pre-authorization. |
increaseAmountrequired | string | Delta to apply, in major units (dollars). |
subtractoptional | string | Set to "1" to decrease instead of increase. |
tipAmountoptional | string | Optional tip to add, in major units. |
curl -X POST https://cloud.handpoint.com/preauthorization/increase \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"originalGuid": "0c9d9df0-48ec-11eb-81a1-470a19c80d3a",
"increaseAmount": "10.00"
}'Pre-auth capture
Finalize an open pre-auth and actually charge the card. capturedAmount is in major units; can’t exceed the authorized amount (after any increases). Add a tip via tipAmount.
| Field | Type | Description |
|---|---|---|
originalGuidrequired | string | GUID of the open pre-authorization. |
capturedAmountrequired | string | Amount to capture, in major units; can't exceed the authorized amount. |
tipAmountoptional | string | Optional tip to add, in major units. |
curl -X POST https://cloud.handpoint.com/preauthorization/capture \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"originalGuid": "0c9d9df0-48ec-11eb-81a1-470a19c80d3a",
"capturedAmount": "120.00"
}'MOTO sale
Charge a previously-tokenized card without involving a terminal. cardToken comes from a prior card-present sale (or the deferred-tokenization endpoint). amount in major units.
| Field | Type | Description |
|---|---|---|
amountrequired | string | Amount to charge, in major units (dollars). |
currencyrequired | string | ISO 4217 currency code. |
cardTokenrequired | string | Token from a prior card-present sale or the deferred-tokenization endpoint. |
transactionReferencerequired | string | UUID v4 for this operation. |
customerReferenceoptional | string | Your own reference for the order. |
curl -X POST https://cloud.handpoint.com/moto/sale \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"amount": "20.00",
"currency": "EUR",
"cardToken": "665630867",
"customerReference": "order-12345",
"transactionReference": "b7b2360d-3e9e-4b62-9a3a-2e6ef6c5cd01"
}'Key entry sale (moToSale)
moToSale without a cardToken shows the terminal’s manual entry screen. Optional channel MO or TO. AVS: send enableAvsFields: true (the terminal asks) or a billing object (zipCode required, address optional). Needs the merchant flags motoEnabled (and avsForMoto for AVS) in TMS; otherwise MOTO_NOT_ENABLED. moToRefund is the keyed refund.
Handpoint has no per-sale switch that shows or hides a keyed option on a normal sale; keyed entry is its own operation. KI-2026-008: poll moToSale by transactionResultId.
curl -X POST https://cloud.handpoint.com/transactions \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"operation": "moToSale",
"amount": "1800",
"currency": "USD",
"terminal_type": "PAXA920MAX",
"serial_number": "2740199794",
"channel": "MO",
"enableAvsFields": true
}'curl -X POST https://cloud.handpoint.com/transactions \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"operation": "moToSale",
"amount": "1800",
"currency": "USD",
"terminal_type": "PAXA920MAX",
"serial_number": "2740199794",
"channel": "TO",
"billing": {
"zipCode": "11933",
"address": "1161 SCOTT AVE"
}
}'# moToSale MOTO_NOT_ENABLED merchant motoEnabled (or avsForMoto for AVS) is off in TMS
MOTO refund
Refund a prior MOTO sale (linked by originalGuid). Amount can’t exceed the original; currency must match.
| Field | Type | Description |
|---|---|---|
originalGuidrequired | string | GUID of the MOTO sale being refunded. |
amountrequired | string | Refund amount in major units; can't exceed the original. |
currencyrequired | string | Must match the original transaction's currency. |
transactionReferencerequired | string | UUID v4 for this operation. |
curl -X POST https://cloud.handpoint.com/moto/refund \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67",
"amount": "5.00",
"currency": "EUR",
"transactionReference": "a1fe8db5-69a4-4b4d-a704-94ac2570f9b0"
}'MOTO reversal
Void a previous MOTO sale within the reversal window. Deprecated by Handpoint: use POST /reversal.
| Field | Type | Description |
|---|---|---|
originalGuidrequired | string | GUID of the MOTO sale to void. |
amountrequired | string | Amount to reverse, in major units. |
currencyrequired | string | ISO 4217 currency code. |
transactionReferencerequired | string | UUID v4 for this operation. |
curl -X POST https://cloud.handpoint.com/moto/reversal \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"originalGuid": "b28bdb10-cf87-11f0-b588-a122fae316de",
"amount": "20.00",
"currency": "EUR",
"transactionReference": "4d7b1a2c-5bfd-4a30-9b6f-123456789abc"
}'Remote reversal
Reverse a sale without the terminal. originalGuid is the sale’s transactionID; amount (major units) is optional and makes it a partial reversal where the acquirer supports it. messageReasonCode: CUSTOMER_CANCELLATION (default) or TIMEOUT_WAITING_FOR_RESPONSE. The call is synchronous. /moto/reversal is deprecated; use /reversal.
| Field | Type | Description |
|---|---|---|
originalGuidrequired | string | transactionID of the sale. |
amountoptional | string | Major units. Omit for the full amount. |
currencyrequired | string | ISO 4217. |
messageReasonCodeoptional | string | CUSTOMER_CANCELLATION (default) | TIMEOUT_WAITING_FOR_RESPONSE. |
Errors: 3158 not found, 3051 already reversed, 4066 partial amount exceeds the original. On-device reversal is saleReversal with originalTransactionId, same business day.
curl -X POST https://cloud.handpoint.com/reversal \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67",
"currency": "USD",
"messageReasonCode": "CUSTOMER_CANCELLATION"
}'curl -X POST https://cloud.handpoint.com/reversal \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67",
"amount": "5.00",
"currency": "USD",
"messageReasonCode": "CUSTOMER_CANCELLATION"
}'# POST /reversal 3158 original transaction not found 3051 already reversed 4066 partial amount exceeds the original
Batch close / summary / detail
Three related endpoints, all POST, all take { deviceType, serialNumber, batchNumber } in the body:
/batch/closeRequest batch closure./batch/summaryTotals + transaction count (same body)./batch/detailArray of every transaction in the batch (filter with optional rrn).All three are Beta and depend on the underlying acquirer supporting them.
| Field | Type | Description |
|---|---|---|
deviceTyperequired | string | Terminal model, e.g. PAXA920MAX. |
serialNumberrequired | string | Terminal serial number. |
batchNumberrequired | string | Batch to act on. |
rrnoptional | string | /batch/detail only: filter to a single transaction by RRN. |
curl -X POST https://cloud.handpoint.com/batch/close \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"deviceType": "PAXA920MAX",
"serialNumber": "2740013262",
"batchNumber": "1"
}'Device commands
Administrative commands for a terminal. Every endpoint returns 202 Accepted when the command is queued. Requires the Handpoint Payments App 4.6.0+ in Integrated mode (or SDK 7.1006.0+). Common error: 400 DeviceNotListening means the terminal isn’t connected right now.
/devices/{type}/{serial}/set-security-policyBody { hideTabBar, blockSystemUI, brightness, passwordToExit }. Needs Payments App 4.14.1 / SDK 7.1014.1./devices/{type}/{serial}/set-unattended-modeKiosk. Body { status: true|false }. Persists across reboot since 4.11.0; locks on power disconnect since 4.14.1./devices/{type}/{serial}/set-localeBody { locale: "en_US" }./devices/{type}/{serial}/set-password-protectedBody { status: true|false }./devices/{type}/{serial}/rebootBody { force: false }; true reboots mid-transaction./devices/{type}/{serial}/set-screen-brightnessBody { minimumBrightnessLevel, maximumBrightnessLevel } (0 to 100)./devices/{type}/{serial}/set-reboot-timeBody { hour: 22 } (0 to 23). Production only.Other POS levers on POST /transactions: stopCurrentTransaction, pingDevice, printReceipt, bypassOptions, tipConfiguration.headerName / footer, a preset tipAmount, and moToSale without a card token (keyed screen). Not available from Handpoint: idle-screen branding or logos, hiding the cancel key, a per-sale keyed option on a normal sale, and pushing customer-display messages.
| Field | Type | Description |
|---|---|---|
deviceTyperequired | string | Path – terminal model. |
serialNumberrequired | string | Path – terminal serial number. |
statusoptional | boolean | Body – set-unattended-mode / set-password-protected: enable or disable. |
hideTabBar / blockSystemUIoptional | boolean | Body – set-security-policy: hide the app’s tab bar; block the Android system UI. |
brightnessoptional | integer | Body – set-security-policy: screen brightness, 0-100. |
passwordToExitoptional | string | Body – set-security-policy: password required to leave the app. |
localeoptional | string | Body – set-locale: IETF BCP 47 tag, e.g. en_CA. |
forceoptional | boolean | Body – reboot: true reboots mid-transaction. |
minimumBrightnessLeveloptional | integer | Body – set-screen-brightness: low end of range, 0-100. |
maximumBrightnessLeveloptional | integer | Body – set-screen-brightness: high end of range, 0-100. |
houroptional | integer | Body – set-reboot-time: daily auto-reboot hour, 0-23 (production only). |
curl -X POST https://cloud.handpoint.com/devices/{deviceType}/{serialNumber}/set-unattended-mode \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"status": true
}'EPI acquirer features
What Handpoint supports when EPI is the acquirer (TSYS). Merchant flags (MOTO, AVS for MOTO, partial reversal, tip adjust, batch) are server-side TMS settings.
| Operation | Cloud | PAX | HiLite | Back office |
|---|---|---|---|---|
| Sale | Yes | Yes | Yes | |
| Refund | Yes | Yes | Yes | |
| Reversal | Yes | Yes | Yes | Yes |
| Tip adjustment | Yes | Yes | Yes | Yes |
| Pre-auth | Yes | Yes | Yes | |
| Pre-auth capture reversal | Yes | Yes | Yes | |
| MOTO | Yes | Yes | ||
| Tokenization | Yes | Yes | Yes | |
| Batch | Yes (US/CA, TMS-enabled) |
Sale fields (Cloud)
amount, currency, terminal_type, serial_number, customerReference, transactionReference, callbackUrl, token, taxInformation, purchaseOrderNumber, duplicate_check, bypassOptions {signatureBypass, pinBypass}, tipConfiguration, merchantAuth, metadata, moneyRemittanceOptions, billing.
| Field | Type | Description |
|---|---|---|
Partial reversaloptional | EPI only | Per-merchant TMS enablement. Non-cumulative. Not on Amex. Cloud, PAX, Cordova, Windows, REST. Errors AMOUNT_EXCEEDS_ORIGINAL, OPERATION_NOT_SUPPORTED, ORIGINAL_NOT_FOUND, 403, 400 “Device is busy”. |
Tokenizationoptional | EPI / PROCHARGE | tokenizeCard (no amount, result PROCESSED) and saleAndTokenizeCard (no tipConfiguration). Deferred: GET /transactions/{transactionID}/token (Cloud 2.26.0). Result cardToken, cardTokenProvider (EPI | PROCHARGE), expiryDateMMYY. Errors TOKENIZATION_NOT_ENABLED, CARD_READ_ERROR. |
Remote saleoptional | MAJOR units | POST /moto/sale {amount, currency, cardToken, transactionReference?, customerReference?, channel?, billing?, taxInformation?, purchaseOrderNumber?}. Errors INVALID_TOKEN, MOTO_NOT_ENABLED. TMS-enabled. |
Remote refundoptional | MAJOR units | POST /moto/refund {originalGuid, amount} within 12 months. KI-2024-005: on sales older than 90 days it returns SUCCESS but is not applied on TSYS. |
EMV refundoptional | terminal | Linked by originalTransactionId (same card enforced) or unlinked by transactionReference. A wallet tap and the physical card produce different PAN tokens. Errors ORIGINAL_NOT_FOUND, UNLINKED_REFUND_NOT_ALLOWED. |
Remote reversaloptional | MAJOR units | See Remote reversal. |
Tipoptional | terminal | tipConfiguration {tipPercentages (required), baseAmount, headerName, enterAmountEnabled, skipEnabled, footer}, or a preset tipAmount that skips the tip screen. The result carries tipAmount and totalAmount. |
Tip adjustmentoptional | MAJOR units | POST /transactions/{transactionID}/tip-adjustment {amount}; 0 voids the tip. Before the EPI batch auto-close (about 11pm EST). To combine with a partial refund: zero the tip, refund, re-adjust. Errors ORIGINAL_NOT_FOUND, BATCH_ALREADY_CLOSED, TIP_ADJUSTMENT_NOT_ENABLED, AMOUNT_EXCEEDS_LIMIT. |
Pre-authoptional | terminal + back office | Create with preAuthorization / moToPreAuthorization. Increase: POST /preauthorization/increase {originalGuid, increaseAmount, subtract "0"|"1", customerReference} returns cumulative holdAmount, increaseAmount, originalAmount. Capture: POST /preauthorization/capture {originalGuid, capturedAmount, tipAmount?, customerReference?, taxInformation?, purchaseOrderNumber? (max 25)}, not above the hold. Void / capture reversal: preAuthorizationReversal or /reversal, full amount, before batch close. Holds expire in 7 to 30 days. Terminal-less increase / capture since Cloud 2.27.0. |
Batch (beta)optional | Cloud 2.25.0 | POST /batch/close {serialNumber, deviceType, batchNumber}; /batch/summary returns transactionCount, netAmount, customFields.salesCount / refundsCount; /batch/detail returns 5 per page, cursor = last retrievalReferenceNumber. |
Level 2optional | Cloud 2.34.0 | taxInformation {taxAmount, taxExempt} + purchaseOrderNumber. Violations return 400. |
Multi-MIDoptional | Cloud, Android PAX | merchantAuth: [{ externalId }], max 23 characters, must match a provisioned subMerchantExternalId. A bad id declines with “Invalid Merchant”, arc 1000. Not echoed in the result. |
Feeoptional | under development | Android PAX SDK fee {amount, mitigationProgram, taxOnFee} only. Cloud does not take fee yet; the flat surchargeAmount still works. |
curl -X POST https://cloud.handpoint.com/transactions \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"operation": "sale",
"amount": "10000",
"currency": "USD",
"terminal_type": "PAXA920MAX",
"serial_number": "2740199794",
"transactionReference": "2bfde1fc-23b1-4c67-93d9-1d4a557f4d4f",
"customerReference": "order-1042",
"duplicate_check": true,
"tipConfiguration": {
"tipPercentages": [
15,
18,
20
],
"headerName": "Add a tip",
"enterAmountEnabled": true,
"skipEnabled": true,
"footer": "Thank you"
},
"taxInformation": {
"taxAmount": "8.25",
"taxExempt": false
},
"purchaseOrderNumber": "PO-55120",
"merchantAuth": [
{
"externalId": "EPI-SUB-0001"
}
],
"bypassOptions": {
"signatureBypass": true,
"pinBypass": false
}
}'curl -X POST https://cloud.handpoint.com/transactions \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"operation": "saleAndTokenizeCard",
"amount": "2500",
"currency": "USD",
"terminal_type": "PAXA920MAX",
"serial_number": "2740199794",
"transactionReference": "7f3a1c22-5b6e-4d0a-9c8b-1e2f3a4b5c6d"
}'curl -X POST https://cloud.handpoint.com/moto/sale \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"amount": "25.00",
"currency": "USD",
"cardToken": "665630867",
"transactionReference": "b7b2360d-3e9e-4b62-9a3a-2e6ef6c5cd01",
"customerReference": "order-1043",
"channel": "MO",
"taxInformation": {
"taxAmount": "2.06",
"taxExempt": false
},
"purchaseOrderNumber": "PO-55121"
}'curl -X POST https://cloud.handpoint.com/moto/refund \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67",
"amount": "5.00"
}'curl -X POST https://cloud.handpoint.com/transactions/1a41d9f0-cf72-11f0-95b2-770b7d1d8e67/tip-adjustment \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"amount": "3.00"
}'curl -X POST https://cloud.handpoint.com/preauthorization/increase \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67",
"increaseAmount": "20.00",
"subtract": "0",
"customerReference": "tab-14"
}'curl -X POST https://cloud.handpoint.com/preauthorization/capture \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"originalGuid": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67",
"capturedAmount": "110.00",
"tipAmount": "10.00",
"customerReference": "tab-14",
"taxInformation": {
"taxAmount": "9.08",
"taxExempt": false
},
"purchaseOrderNumber": "PO-55122"
}'curl -X POST https://cloud.handpoint.com/batch/summary \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"serialNumber": "2740199794",
"deviceType": "PAXA920MAX",
"batchNumber": "112"
}'Paysafe (Handpoint direct)
For merchants whose acquirer on Handpoint is Paysafe. Paysafe merchants integrate with Handpoint directly; they are not on Merchant360.
| Operation | Surfaces |
|---|---|
| Sale, sale with tip, refund, reversal, tip adjustment, tokenization | Cloud, PAX, HiLite |
| Void | Interac only |
| MOTO / key entry | Cloud, PAX, JavaScript, Windows |
| Remote refund | /moto/refund, within 12 months |
| Not supported | Partial reversal, pre-auth (listed as coming soon), remote sale, batch |
Tokens
- PAYSAFE:
tokenizeCard/saleAndTokenizeCardreturncardTokenProvider: "PAYSAFE", a single-use token that lives about 15 minutes, for Paysafe’s Customer Vault. It does not work with Handpoint MOTO. Needs Paysafe onboarding. - TOKENEX:
cardTokenProvider: "TOKENEX", for loyalty and card matching only. No detokenize, no MOTO.
Interac
- Interac routes to TNS; other cards to TSYS. Batch close is TSYS only.
- Interac supports sale, sale with tip (the tip is folded into the total) and VOID only.
- VOID = Cloud
refundwithoriginalTransactionId,currency: "CAD", notransactionReference. Card present, same day, full amount. Label the button “VOID” in your UI. - No remote reversal or refund, pre-auth, tip adjust or keyed entry for Interac.
- Errors:
CARD_NOT_PRESENT,ALREADY_SETTLED,ORIGINAL_NOT_FOUND.
Refunds in the Paysafe Portal are card-not-present and outside Handpoint; after 24 h they use the eftTransactionID as MerchantRefNum.
curl -X POST https://cloud.handpoint.com/transactions \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"operation": "refund",
"amount": "2500",
"currency": "CAD",
"terminal_type": "PAXA920PRO",
"serial_number": "1850012345",
"originalTransactionId": "1a41d9f0-cf72-11f0-95b2-770b7d1d8e67"
}'curl -X POST https://cloud.handpoint.com/transactions \
-H "ApiKeyCloud: <merchant-api-key>" \
-H "Content-Type: application/json" \
-d '{
"operation": "tokenizeCard",
"terminal_type": "PAXA920PRO",
"serial_number": "1850012345"
}'# Interac VOID CARD_NOT_PRESENT the card must be presented on the terminal ALREADY_SETTLED VOID is same day only ORIGINAL_NOT_FOUND originalTransactionId not found
TMS API
Terminal Management System – create merchants, assign terminals, mint API keys for the REST surface. Provisioning only; no payments cross this API.
POST /loginLogin
Login returns a JSON Web Token. Send it in the Authorization: Bearer … header on every other TMS call. Tokens have a fixed lifetime – handle re-login when calls return 401.
# 1. Login (credentials → JWT) POST /login # 2. Every subsequent call: Authorization: Bearer <jwt>
Merchants
Merchant lifecycle. The POST /merchants body must match your partner configuration (ISV id, acquirer id, etc.) – ask your Handpoint contact for the exact shape.
GET /merchants/{id} returns the merchant’s shared secret and other auth-grade fields; treat the response like a credential. After PUT, re-publish each assigned terminal’s configuration (see Lifecycle below).
POST /merchants create
GET /merchants list all under the partner
GET /merchants/{id} retrieve profile (includes shared secret)
PUT /merchants/{id} update – re-publish terminals after
DELETE /merchants/{id} must have zero assigned terminalsTerminals
Each terminal lives in one of two states: unassigned (in the pool, no merchant) or assigned (attached to exactly one merchant). Reassignment is unassign → assign.
You must call POST /publish after every assign / unassign / merchant-edit. Without it the Handpoint Gateway doesn’t know about the change and the terminal will keep authorizing under the old merchant context.
POST /terminals add to unassigned pool
GET /terminals/unassigned list pool (searchable)
POST /terminals/{id}/assign attach to a merchant
POST /terminals/{id}/publish push config to Gateway
(REQUIRED after assign / change)
POST /terminals/{id}/unassign back to the pool
GET /merchants/{m}/terminals list assigned (searchable)
DELETE /terminals/{id} terminal must be unassigned firstAPI keys
Cloud API keys are minted per merchant from the TMS. The POST response includes the unmasked key – that’s the only time you’ll see it. Store it server-side before handing to the merchant.
POST /merchants/{id}/api-keys mint a new Cloud API key
GET /merchants/{id}/api-keys list (masked)
DELETE /merchants/{id}/api-keys/{key} revokeLifecycle workflow
Standard onboarding for a new merchant + first terminal is six API calls in sequence:
- Login → JWT.
- Create the merchant.
- Add the terminal to the unassigned pool.
- Assign the terminal to the merchant.
- Publish the terminal config to the Gateway.
- Mint the merchant’s REST API key.
# 1. Login → JWT
# 2. POST /merchants → create
# 3. POST /terminals → add terminal to pool
# 4. POST /terminals/{id}/assign → attach to merchant
# 5. POST /terminals/{id}/publish → tell the Gateway
# 6. POST /merchants/{id}/api-keys → mint REST key for the merchant
# Ongoing – after ANY merchant edit or terminal assignment change:
# re-publish each affected terminal.Transaction Feed API
Read-only reporting side – pull transactions, run aggregates, build dashboards. Separate analytics key from the REST API; cannot take payments. API version 2.8.0.
GET /transactions on the Merchant360 API returns the merchant’s Handpoint reader sales together with card, ACH, cash, check, gift card and eGift activity, in one shape, with a Merchant360 key. Documentation: https://www.cygma.cloud/merchant360#transactions
Authentication
Different credential from the REST API. The Transaction Feed is a read-only reporting surface; the key here doesn’t let you take payments. Ask Handpoint to provision a separate Analytics key – don’t reuse the Cloud API key.
curl -X POST https://txnfeed.handpoint.io/transactions \
-H "AnalyticsApiKey: Your-Api-Key" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"startDate": "20260101000000",
"endDate": "20260201000000"
}'Request config
Date format is a single string of digits: YYYYMMDDhhmmss. 20260609143200 = June 9, 2026 at 14:32:00. Both startDate and endDate are required on every POST.
| Field | Type | Description |
|---|---|---|
startDaterequired | string | Window start, YYYYMMDDhhmmss (14 digits, 24-hour, no separators). |
endDaterequired | string | Window end, same format. |
// Every POST takes a RequestConfig body
{
"startDate": "20260101000000",
"endDate": "20260201000000"
}
// Format: YYYYMMDDhhmmss (14 chars, 24-hour, no separators).Transaction lookups
Five basic lookups, two type-filters (payouts = income, refundsandreversals = outflow), plus admin variants scoped to a specific merchant or to the partner level.
Every endpoint is POST so the body can carry the date range. The path parameters (guid, customerReference, partnerIdAlpha, merchantIdAlpha) sit in the URL.
| Field | Type | Description |
|---|---|---|
guidoptional | string | Path – single-transaction endpoints: the transaction GUID. |
customerReferenceoptional | string | Path – by-reference endpoints: your own reference. |
partnerIdAlphaoptional | string | Path – admin endpoints: partner scope. |
merchantIdAlphaoptional | string | Path – admin endpoints: merchant scope. |
guidsoptional | array | Body – /transactions/guids: bulk list of GUIDs. |
customerReferencesoptional | array | Body – /transactions/customerreferences: bulk list of references. |
POST /transactions # all transactions in window
POST /transactions/{guid} # one by guid
POST /transactions/guids # bulk by guid (body)
POST /transactions/customerreference/{customerReference} # by your reference
POST /transactions/customerreferences # bulk by your reference
# Pre-filtered by direction:
POST /transactions/payouts # sales + captures
POST /transactions/refundsandreversals # refunds + reversals
# Admin endpoints (require partner + merchant ids):
POST /transactions/{partnerIdAlpha}/{merchantIdAlpha}
POST /transactions/{partnerIdAlpha}/{merchantIdAlpha}/payouts
POST /transactions/{partnerIdAlpha}/{merchantIdAlpha}/refundsandreversals
# Partner-or-merchant context:
POST /transactions/partnerormerchant
POST /transactions/partnerormerchant/payouts
POST /transactions/partnerormerchant/refundsandreversalsAggregations
Five aggregation families. {field} is the JSON attribute you’re aggregating over (amount, serialNumber, cardType, etc.). Net is a built-in convenience: payouts minus refunds+reversals.
| Field | Type | Description |
|---|---|---|
fieldrequired | string | JSON attribute to aggregate over, e.g. amount, serialNumber, cardType. (Count endpoints omit it.) |
# Avg
POST /transactions/aggregate/avg/{field}
POST /transactions/aggregate/avg/payouts/{field}
POST /transactions/aggregate/avg/refundsandreversals/{field}
# Sum
POST /transactions/aggregate/sum/{field}
POST /transactions/aggregate/sum/payouts/{field}
POST /transactions/aggregate/sum/refundsandreversals/{field}
# Count
POST /transactions/count
POST /transactions/count/payouts
POST /transactions/count/refundsandreversals
# Distinct count
POST /transactions/aggregate/distinctcount/{field}
# Net (payouts − refundsandreversals)
POST /transactions/aggregate/net/{field}Grouping + time series
Add /group/ to the path and supply three query params:
| Field | Type | Description |
|---|---|---|
oprequired | string | Path – aggregation op: avg, sum, count, distinctcount, or net. |
fieldrequired | string | Path – JSON attribute to aggregate over (omit for count). |
groupByrequired | string | Query – comma-separated field names. Multiple keys = multiple group dimensions. |
calendarIntervaloptional | string | Query – time bucket size, e.g. HOUR, DAY, MONTH. |
orderoptional | string | Query – ASC or DESC. |
# Generic grouped aggregate (works for avg/sum/count/distinctcount/net)
POST /transactions/aggregate/group/{op}/{field}?groupBy=serialNumber,cardType&calendarInterval=DAY&order=DESC
# Variants:
/aggregate/group/avg/{field}
/aggregate/group/avg/payouts/{field}
/aggregate/group/avg/refundsandreversals/{field}
/aggregate/group/sum/{field}
/aggregate/group/count
/aggregate/group/distinctcount/{field}
/aggregate/group/net/{field}Graph endpoints
Same shape as /group/ but the response is pre-bucketed for charting libraries (timestamps on the X axis, the aggregate on Y). Skip the per-row mapping you’d normally do for a graph.
| Field | Type | Description |
|---|---|---|
fieldrequired | string | Path – JSON attribute to aggregate over (omit for count). |
groupByrequired | string | Query – comma-separated field names. |
calendarIntervaloptional | string | Query – time bucket size, e.g. DAY. |
orderoptional | string | Query – ASC or DESC. |
POST /transactions/aggregate/group/graph/avg/{field}
POST /transactions/aggregate/group/graph/sum/{field}
POST /transactions/aggregate/group/graph/count
POST /transactions/aggregate/group/graph/distinctcount/{field}
POST /transactions/aggregate/group/graph/net/{field}
# Same groupBy / calendarInterval / order params as the
# non-graph variants. Response shape is pre-bucketed for
# direct charting.Errors
| Status | Meaning |
|---|---|
| 200 | OK |
| 202 | Accepted – operation queued, result via callback or polling. |
| 204 | No content – used by the polling result endpoint while in-flight. |
| 400 | Bad request. REST error codes: 1001 busy, 1002 offline, 1003 cancel not allowed, 3107 CVV required, 3209 amount exceeds original, 3210 currency mismatch, 3211 pre-auth closed, 5252 token failure. |
| 403 | Authentication failed or insufficient permissions. |
| 404 | Resource not found (transactionResultId, transactionReference, etc.). |
| 422 | Unprocessable – missing required fields. |
| 5xx | Internal Handpoint error. |