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 APIfor 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. Because the PAN never touches the merchant’s server, the integrator stays out of PCI scope.
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.
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 hitcloud.handpoint.com; debug terminals hitcloud.handpoint.io. Demo terminals use a mock acquirer — no funds move, but every other behavior is real.
# 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.io
Flow models (push vs poll)
Every transaction request returns 202 Accepted with atransactionResultId— 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"
}REST API
Taking payments on physical terminals — sale, refund, void, pre-auth, MOTO, batch, and device controls.
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 operationvalues include sale, refund,refundReversal, preAuthorizationCapture, and more.
amount is in minor units (cents) as a string:"10000" = €100.00. transactionReferencemust 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 a200. Returns the full transaction-result object with masked PAN, card scheme, AVS / CVV results, signature URL, customer + merchant receipt text, and the cardTokenyou 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 transactionReferenceyou supplied on the original request. Useful when you’ve lost thetransactionResultId 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: AUTHORISED, DECLINED, UNDEFINED, IN_PROGRESS, REFUNDED, CANCELLED.
| Field | Type | Description |
|---|---|---|
transactionReferencerequired | string | Path — the reference you supplied on the original request. |
selectoroptional | string | Query — which linked op to return: first (default), last, all, or a numeric index. |
curl -X GET https://transactions.handpoint.com/transactions/{transactionReference}/status \
-H "ApiKeyCloud: <merchant-api-key>"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.
| 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 inmajor 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.capturedAmountis in major units; can’t exceed the authorized amount (after any increases). Add a tip viatipAmount.
| 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"
}'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.
| 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"
}'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 controls
Six administrative controls for a terminal — every endpoint returns 202 Accepted when the command is queued. Common error: 400 DeviceNotListening means the terminal isn’t connected right now.
/devices/{deviceType}/{serialNumber}/set-unattended-modeHide navigation, show payment only. Body { status: true|false }./devices/{deviceType}/{serialNumber}/set-localeSet locale (IETF BCP 47 tag). Body { locale: "en_CA" }./devices/{deviceType}/{serialNumber}/set-password-protectedToggle password protection. Body { status: true|false }./devices/{deviceType}/{serialNumber}/rebootReboot; force: true reboots mid-transaction. Body { force: false }./devices/{deviceType}/{serialNumber}/set-screen-brightnessScreen brightness range 0-100. Body { minimumBrightnessLevel, maximumBrightnessLevel }./devices/{deviceType}/{serialNumber}/set-reboot-timeDaily auto-reboot hour 0-23 (production only). Body { hour: 22 }.| 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. |
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
}'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 theAuthorization: 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. ThePOSTresponse 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.
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 andendDate 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. |