Card-present terminals · TMS · reporting

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.

Overview

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.
One credential per surface

Authentication

Each surface uses a different credential and a different header:

  • REST APIApiKeyCloud header. The key is unique per merchant; you mint it through the TMS API.
  • TMS API — JWT obtained from a Login endpoint, passed in Authorization: Bearer on subsequent calls.
  • Transaction Feed APIAnalyticsApiKey header. 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>
Production vs debug

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
Callback or polling

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 + token in the request. Handpoint POSTs the result to your URL when the terminal finishes; the token echoes in AUTH-TOKEN so 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. Returns 204 while in flight, 200 with 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"
}
01
API surface 1 of 3

REST API

Taking payments on physical terminals — sale, refund, void, pre-auth, MOTO, batch, and device controls.

cloud.handpoint.com·Auth: ApiKeyCloud header
GET /devices

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>"
POST /transactions

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.

Body parameters
FieldTypeDescription
operationrequiredstringTerminal operation: sale, refund, refundReversal, preAuthorizationCapture, etc.
amountrequiredstringAmount in minor units (cents) as a string. "10000" = €100.00.
currencyrequiredstringISO 4217 currency code, e.g. EUR.
terminal_typerequiredstringTerminal model from List devices, e.g. PAXA920.
serial_numberrequiredstringTerminal serial number from List devices.
transactionReferencerequiredstringUUID v4 for original operations; echoed back on the result.
customerReferenceoptionalstringYour own reference for the operation.
originalTransactionIdoptionalstringLinks 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"
}'
GET /transaction-result/{id}

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.

Path parameters
FieldTypeDescription
idrequiredstringThe transactionResultId returned by POST /transactions.
curl -X GET https://cloud.handpoint.com/transaction-result/1850025030-1776238708721 \
  -H "ApiKeyCloud: <merchant-api-key>"
GET /transactions/{ref}/status

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.

FieldTypeDescription
transactionReferencerequiredstringPath — the reference you supplied on the original request.
selectoroptionalstringQuery — 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>"
POST /transactions/{guid}/tip-adjustment · US only

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.

FieldTypeDescription
guidrequiredstringPath — GUID of the authorized sale to adjust.
amountrequiredstringBody — 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"
}'
GET /transactions/{guid}/token

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.

Path parameters
FieldTypeDescription
guidrequiredstringGUID of the completed transaction to tokenize.
curl -X GET https://cloud.handpoint.com/transactions/{guid}/token \
  -H "ApiKeyCloud: <merchant-api-key>"
POST /preauthorization/increase

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.

Body parameters
FieldTypeDescription
originalGuidrequiredstringGUID of the open pre-authorization.
increaseAmountrequiredstringDelta to apply, in major units (dollars).
subtractoptionalstringSet to "1" to decrease instead of increase.
tipAmountoptionalstringOptional 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"
}'
POST /preauthorization/capture

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.

Body parameters
FieldTypeDescription
originalGuidrequiredstringGUID of the open pre-authorization.
capturedAmountrequiredstringAmount to capture, in major units; can't exceed the authorized amount.
tipAmountoptionalstringOptional 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"
}'
POST /moto/sale

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.

Body parameters
FieldTypeDescription
amountrequiredstringAmount to charge, in major units (dollars).
currencyrequiredstringISO 4217 currency code.
cardTokenrequiredstringToken from a prior card-present sale or the deferred-tokenization endpoint.
transactionReferencerequiredstringUUID v4 for this operation.
customerReferenceoptionalstringYour 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"
}'
POST /moto/refund

MOTO refund

Refund a prior MOTO sale (linked by originalGuid). Amount can’t exceed the original; currency must match.

Body parameters
FieldTypeDescription
originalGuidrequiredstringGUID of the MOTO sale being refunded.
amountrequiredstringRefund amount in major units; can't exceed the original.
currencyrequiredstringMust match the original transaction's currency.
transactionReferencerequiredstringUUID 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"
}'
POST /moto/reversal

MOTO reversal

Void a previous MOTO sale within the reversal window.

Body parameters
FieldTypeDescription
originalGuidrequiredstringGUID of the MOTO sale to void.
amountrequiredstringAmount to reverse, in major units.
currencyrequiredstringISO 4217 currency code.
transactionReferencerequiredstringUUID 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"
}'
Beta · acquirer-dependent

Batch close / summary / detail

Three related endpoints, all POST, all take { deviceType, serialNumber, batchNumber } in the body:

POST/batch/closeRequest batch closure.
POST/batch/summaryTotals + transaction count (same body).
POST/batch/detailArray of every transaction in the batch (filter with optional rrn).

All three are Beta and depend on the underlying acquirer supporting them.

Body parameters
FieldTypeDescription
deviceTyperequiredstringTerminal model, e.g. PAXA920MAX.
serialNumberrequiredstringTerminal serial number.
batchNumberrequiredstringBatch to act on.
rrnoptionalstring/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"
}'
Six POST endpoints

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.

POST/devices/{deviceType}/{serialNumber}/set-unattended-modeHide navigation, show payment only. Body { status: true|false }.
POST/devices/{deviceType}/{serialNumber}/set-localeSet locale (IETF BCP 47 tag). Body { locale: "en_CA" }.
POST/devices/{deviceType}/{serialNumber}/set-password-protectedToggle password protection. Body { status: true|false }.
POST/devices/{deviceType}/{serialNumber}/rebootReboot; force: true reboots mid-transaction. Body { force: false }.
POST/devices/{deviceType}/{serialNumber}/set-screen-brightnessScreen brightness range 0-100. Body { minimumBrightnessLevel, maximumBrightnessLevel }.
POST/devices/{deviceType}/{serialNumber}/set-reboot-timeDaily auto-reboot hour 0-23 (production only). Body { hour: 22 }.
FieldTypeDescription
deviceTyperequiredstringPath — terminal model.
serialNumberrequiredstringPath — terminal serial number.
statusoptionalbooleanBody — set-unattended-mode / set-password-protected: enable or disable.
localeoptionalstringBody — set-locale: IETF BCP 47 tag, e.g. en_CA.
forceoptionalbooleanBody — reboot: true reboots mid-transaction.
minimumBrightnessLeveloptionalintegerBody — set-screen-brightness: low end of range, 0-100.
maximumBrightnessLeveloptionalintegerBody — set-screen-brightness: high end of range, 0-100.
houroptionalintegerBody — 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
}'
02
API surface 2 of 3

TMS API

Terminal Management System — create merchants, assign terminals, mint API keys for the REST surface. Provisioning only; no payments cross this API.

tmsdoc.handpoint.com·Auth: Bearer JWT from POST /login
JWT auth

Login

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>
Create · read · update · delete

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 terminals
Create · assign · publish · unassign · delete

Terminals

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 first
Mint REST API + Txn Feed keys

API 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}     revoke
Standard onboarding

Lifecycle 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.
03
API surface 3 of 3

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.

txnfeed.handpoint.io·Auth: AnalyticsApiKey header
Header-based

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"
}'
Date-range filter

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.

Body parameters
FieldTypeDescription
startDaterequiredstringWindow start, YYYYMMDDhhmmss (14 digits, 24-hour, no separators).
endDaterequiredstringWindow end, same format.
// Every POST takes a RequestConfig body
{
  "startDate": "20260101000000",
  "endDate":   "20260201000000"
}

// Format: YYYYMMDDhhmmss (14 chars, 24-hour, no separators).
Five flavors

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.

FieldTypeDescription
guidoptionalstringPath — single-transaction endpoints: the transaction GUID.
customerReferenceoptionalstringPath — by-reference endpoints: your own reference.
partnerIdAlphaoptionalstringPath — admin endpoints: partner scope.
merchantIdAlphaoptionalstringPath — admin endpoints: merchant scope.
guidsoptionalarrayBody — /transactions/guids: bulk list of GUIDs.
customerReferencesoptionalarrayBody — /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/refundsandreversals
Avg · Sum · Count · Distinct count · Net

Aggregations

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.

Path parameters
FieldTypeDescription
fieldrequiredstringJSON 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}
groupBy + calendarInterval

Grouping + time series

Add /group/ to the path and supply three query params:

FieldTypeDescription
oprequiredstringPath — aggregation op: avg, sum, count, distinctcount, or net.
fieldrequiredstringPath — JSON attribute to aggregate over (omit for count).
groupByrequiredstringQuery — comma-separated field names. Multiple keys = multiple group dimensions.
calendarIntervaloptionalstringQuery — time bucket size, e.g. HOUR, DAY, MONTH.
orderoptionalstringQuery — 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}
Visualization-ready

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.

FieldTypeDescription
fieldrequiredstringPath — JSON attribute to aggregate over (omit for count).
groupByrequiredstringQuery — comma-separated field names.
calendarIntervaloptionalstringQuery — time bucket size, e.g. DAY.
orderoptionalstringQuery — 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.
Common status codes

Errors

StatusMeaning
200OK
202Accepted — operation queued, result via callback or polling.
204No content — used by the polling result endpoint while in-flight.
400Bad 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.
403Authentication failed or insufficient permissions.
404Resource not found (transactionResultId, transactionReference, etc.).
422Unprocessable — missing required fields.
5xxInternal Handpoint error.