Terminal payments
This guide walks you through integrating card payments on payment terminals into your cash register (POS) using the Qerko POS API. Your cash register sends the amount, the customer pays on the terminal, and your cash register gets the result with everything needed for the receipt.
The guide covers the whole process:
- Before you start – the API key and
Pos-Id - Sending requests – authentication and how responses work
- Getting the terminals
- Checking a terminal (optional)
- Payments
- Reversals – cancelling a payment in full
- Refunds – returning money (or a part of it) back to the card
- Closings – closing the terminal batch
- Errors when starting an operation
- Recommendations
The full reference of all methods and fields is in the API Reference under the Terminal payments section.
How it works
You don’t talk to the terminal directly. Every operation (payment, reversal, refund, closing) works the same way:
- Your cash register starts the operation with a
create-terminal-*method. The method returns right away. The operation then runs on the terminal. - Qerko drives the operation to completion in the background: it follows the terminal, gets the result, confirms it and releases the terminal.
- Your cash register polls the operation’s state with the matching
get-terminal-*-statusmethod until the state is final.
Each operation is identified by a transactionId that your cash register generates (a UUID). You use the same transactionId to poll the operation’s state.
Before you start
API key
Terminal payments use a dedicated API key with the Terminal payments role. It is a different key from the standard key your POS uses for the other Qerko services.
- How to get it: Qerko issues the key on request, and it cannot be generated in Restaurant Admin. To request one, contact Qerko.
- Which company: the key belongs to a company (legal entity), not to a single restaurant. It gives access to the terminals of all active restaurants (locations) of that company.
- Where it shows up: the merchant can see the key in Restaurant Admin under API keys, with the role Terminal payments. The merchant can also delete it there, and a deleted key stops working right away.
- Keep it secret: treat the key as a secret. Store it only in your cash register’s configuration and never show it to customers.
Pos-Id
Every request also carries the Pos-Id of your cash register system, as for the rest of the POS API.
In the sandbox environment, you can use any UUID as the Pos-Id.
Terminals
A terminal can be used only when the integration is enabled for it and it runs in the integrated mode. Qerko takes care of both when setting the terminal up. Your cash register finds out which terminals are ready from /get-terminals (enabled: true).
Environments
| Environment | Base URL |
|---|---|
| Sandbox | https://sandbox.qerko.com/api/v4/pos |
| Production | https://qerko.com/api/v4/pos |
Sending requests
All methods are POST requests with a JSON body (an empty object {} when the method takes no data). Every request carries two headers:
Responses
- Every business outcome is returned as HTTP
200. Outcomes are told apart by thetypefield, for examplestarted,unknownTerminalorterminalBusy. Always switch ontype. Don’t rely on the HTTP status alone. - The state of an operation is in its
statusfield:pendingwhile it runs, then a final status (approved/completed,failedorunknown). 401means a missing or invalidPos-Idor API key, or a key without the Terminal payments role.400means the request data is invalid (for example a missing field or an amount with more than 2 decimal places).
Data formats
- Amounts are decimal strings with at most 2 decimal places, for example
"249.90". - Currencies are ISO 4217 codes:
CZK,EUR,HUF,PLN,USD. - Dates are ISO 8601 strings in UTC, for example
"2026-10-14T09:15:22.123Z". - IDs (
transactionId, …) are UUIDs.
Step 1: Get the terminals
POST /get-terminals returns the company the API key belongs to, its active locations (restaurants) and their terminals.
Request
{}Response
{
"legalEntity": {
"id": "8d0d5c2e-6f4b-4b8e-9c55-1f1c1c0a7b21",
"name": "Bistro s.r.o.",
"identificationNumber": "12345678",
"country": "CZ"
},
"locations": [
{
"id": "3a7e2f4c-1b2d-4e5f-8a9b-0c1d2e3f4a5b",
"name": "Bistro Karlín",
"address": {
"street": "Křižíkova",
"descriptiveNumber": "12",
"city": "Praha",
"zipCode": "18600",
"country": "CZ"
},
"terminals": [
{
"id": "A1B2C3D4",
"tid": "A1B2C3D4",
"platform": "physical",
"enabled": true
}
]
}
]
}| Field | Meaning |
|---|---|
id | Terminal identifier. Send it as terminalId to the other methods. Treat it as an opaque value: today it equals tid, but it may change to a different identifier later. Don’t parse it or derive it from tid. |
tid | Terminal ID as shown on the terminal and on the receipts, for information only. |
platform | physical for a payment terminal, softPos for a tap-to-pay app on an Android phone. |
enabled | Whether the terminal can be used for terminal payments. Only terminals with enabled: true work with the other methods. |
Typically the cash register lets the staff pick a terminal from this list once, during setup, and stores its id. Call /get-terminals again when the setup changes.
Step 2 (optional): Check the terminal
POST /get-terminal-status checks that the terminal is connected. Optionally, it also checks that the terminal reaches the authorization server. Use it, for example, in the cash register’s setup screen to test the connection.
Request
{
"terminalId": "A1B2C3D4",
"checkAuthorizationServer": true
}Response
{
"type": "status",
"terminalId": "A1B2C3D4",
"terminalDateTime": "2026-10-14T09:15:22.000Z",
"authorizationServerConnection": "ok",
"authorizationServerMessage": "Connection OK"
}authorizationServerConnection:notChecked(whencheckAuthorizationServerisfalse),okorerror.- Other outcomes:
unknownTerminal,terminalNotActivated,terminalBusy,terminalNotConnected,upstreamTimeoutandupstreamError. See Errors when starting an operation. - During an operation: while a payment or another operation runs on the terminal, the check returns
terminalBusy.
Step 3: Payments
Starting a payment
POST /create-terminal-payment starts a payment on the terminal. The customer then pays on the terminal.
Request
| Field | Required | Meaning |
|---|---|---|
terminalId | yes | id of the terminal from /get-terminals |
transactionId | yes | A new UUID generated by your cash register for this payment. It must be unique. A reused value ends with duplicateTransaction. |
amount | yes | Total amount, including tipAmount |
currency | yes | Currency of the payment |
tipAmount | no | Tip included in amount |
referenceId | no | Your reference, for example the bill number, at most 20 characters. It doesn’t have to be unique, so a bill split into several payments can share it. |
Response: the payment was started.
{
"type": "started",
"payment": {
"status": "pending",
"step": "connecting",
"transactionId": "0f8b7c1e-5a4d-4c3b-9e2f-7a6b5c4d3e2f",
"referenceId": "BILL-2026-001234",
"amount": "270.00",
"tipAmount": "20.00",
"currency": "CZK",
"createdAt": "2026-10-14T09:15:22.123Z"
}
}If the payment did not start, type says why:
unknownTerminal: the terminal doesn’t exist, isn’t enabled, or belongs to another company.notStarted: the payment processor refused the payment, for example because of an invalid amount or an unsupported currency.- Other errors: see Errors when starting an operation.
Following the payment
POST /get-terminal-payment-status returns the current state of the payment. It reads the state stored by Qerko and does not contact the terminal, so you can call it as often as you need. We recommend polling once per second until the status is final.
Request
{
"transactionId": "0f8b7c1e-5a4d-4c3b-9e2f-7a6b5c4d3e2f"
}Response: type is found with the payment, or unknownTransaction when no payment with this transactionId was started.
While the payment runs (status: pending), step tells you what is happening on the terminal. You can show it to the staff:
step | Meaning |
|---|---|
connecting | The terminal is connecting to the payment processor |
waitingForCard | Waiting for the customer to tap or insert the card |
waitingForPin | Waiting for the customer to enter the PIN |
waitingForCustomer | Waiting for another customer input on the terminal |
authorizing | The payment is being authorized |
finishing | The result is known and the payment is being finished |
Payment results
status | Meaning | What the cash register does |
|---|---|---|
pending | The payment is running | Keep polling |
approved | The card was charged and the payment is confirmed | Mark the bill as paid and print the receipt |
failed | The card was not charged | Tell the staff and let them try again or pick another payment method |
unknown | The result could not be determined | The staff must check the payment on the terminal (in its transaction history) before closing the bill |
Approved payment
{
"type": "found",
"payment": {
"status": "approved",
"transactionId": "0f8b7c1e-5a4d-4c3b-9e2f-7a6b5c4d3e2f",
"referenceId": "BILL-2026-001234",
"amount": "270.00",
"tipAmount": "20.00",
"currency": "CZK",
"createdAt": "2026-10-14T09:15:22.123Z",
"finishedAt": "2026-10-14T09:15:41.456Z",
"chargedAmount": "270.00",
"chargedTipAmount": "20.00",
"authorizationCode": "123456",
"card": {
"brand": "VISA",
"maskedPan": "************8902",
"panSequenceNumber": "01",
"entryMode": "contactless",
"aid": "A0000000031010",
"applicationLabel": "VISA CREDIT"
},
"cardholderVerification": ["none"],
"receipt": {
"merchantId": "000123456",
"terminalId": "A1B2C3D4",
"sequenceNumber": "000042",
"terminalDateTime": "2026-10-14T09:15:40.000Z",
"refundId": "R-7F3A9C21"
},
"reversalTransactionId": null
}
}| Field | Meaning |
|---|---|
chargedAmount, chargedTipAmount | The amount actually charged, including the tip. It can differ from amount when the customer added a tip on the terminal. Use these values for the bill. |
authorizationCode | Authorization code for the receipt |
card | Card brand, masked card number, how the card was read (entryMode: contactless, chip or magstripe) and EMV data for the receipt |
cardholderVerification | How the cardholder was verified: none (for example a low-value contactless payment), signature, pinOnline, pinOffline or consumerDevice |
receipt | Data for the receipt: merchant ID and terminal ID at the acquirer, sequence number and the time on the terminal |
receipt.refundId | Identifier you need for a later refund. Store it with the payment. |
reversalTransactionId | transactionId of the reversal when the payment was later reversed, otherwise null |
Failed payment
{
"type": "found",
"payment": {
"status": "failed",
"reason": "declined",
"transactionId": "0f8b7c1e-5a4d-4c3b-9e2f-7a6b5c4d3e2f",
"referenceId": "BILL-2026-001234",
"amount": "270.00",
"tipAmount": "20.00",
"currency": "CZK",
"createdAt": "2026-10-14T09:15:22.123Z",
"finishedAt": "2026-10-14T09:15:35.001Z"
}
}reason | Meaning |
|---|---|
declined | Declined by the card issuer or the terminal |
cancelled | Cancelled on the terminal or by the customer |
reversed | Authorized, but reversed by the payment processor (for example because it could not be confirmed) |
In every case the card was not charged, so you can start a new payment with a new transactionId.
Step 4: Reversals
A reversal cancels an approved payment in full. It runs on the same terminal that made the payment, and the customer’s card is not needed. Use it, for example, when a payment was made by mistake.
POST /create-terminal-reversal
Request
{
"transactionId": "5c2d1e0f-9a8b-4c7d-8e6f-1a2b3c4d5e6f",
"originalTransactionId": "0f8b7c1e-5a4d-4c3b-9e2f-7a6b5c4d3e2f"
}| Field | Meaning |
|---|---|
transactionId | A new UUID for the reversal |
originalTransactionId | transactionId of the payment to reverse |
Response: type: started with the reversal (status: pending). If the reversal did not start, type says why:
type | Meaning |
|---|---|
originalTransactionNotFound | No payment with originalTransactionId exists for this company |
originalTransactionNotReversible | The payment isn’t approved, so there is nothing to reverse |
alreadyReversed | The payment was already reversed, or its reversal is still running |
terminalUnavailable | The terminal of the payment is no longer active or enabled |
notStarted | The payment processor refused the reversal, for example because the payment can no longer be reversed |
| other | See Errors when starting an operation |
Poll POST /get-terminal-reversal-status with the reversal’s transactionId:
{
"type": "found",
"reversal": {
"status": "approved",
"transactionId": "5c2d1e0f-9a8b-4c7d-8e6f-1a2b3c4d5e6f",
"originalTransactionId": "0f8b7c1e-5a4d-4c3b-9e2f-7a6b5c4d3e2f",
"amount": "270.00",
"currency": "CZK",
"createdAt": "2026-10-14T09:20:02.000Z",
"finishedAt": "2026-10-14T09:20:06.000Z",
"card": {
"brand": "VISA",
"maskedPan": "************8902",
"panSequenceNumber": "01",
"entryMode": "contactless",
"aid": "A0000000031010"
},
"receipt": {
"merchantId": "000123456",
"terminalId": "A1B2C3D4"
}
}
}status | Meaning |
|---|---|
pending | The reversal is running |
approved | The payment was reversed and the charged amount (amount) was returned to the card. finishedAt is the time of the reversal for the receipt. The authorization code and sequence number of the original payment are in /get-terminal-payment-status. |
failed | The reversal did not go through (reason: declined or cancelled), so the payment stays charged |
unknown | The result could not be determined and has to be checked on the terminal |
After an approved reversal, /get-terminal-payment-status of the payment returns the reversal’s transactionId in reversalTransactionId.
Step 5: Refunds
A refund returns money (the full amount or a part of it) back to the card of an earlier payment.
- How the payment is identified: by its
refundIdfrom the payment receipt (receipt.refundIdof the approved payment). - Which terminal: the card is not presented, so the refund can run on any enabled terminal of the company.
POST /create-terminal-refund
Request
{
"terminalId": "A1B2C3D4",
"transactionId": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
"refundId": "R-7F3A9C21",
"amount": "50.00",
"currency": "CZK",
"referenceId": "CN-2026-000017"
}| Field | Required | Meaning |
|---|---|---|
terminalId | yes | Terminal to run the refund on |
transactionId | yes | A new UUID for the refund |
refundId | yes | refundId from the receipt of the payment to refund |
amount | yes | Amount to refund |
currency | yes | Currency of the refund |
referenceId | no | Your reference, for example a credit note number, at most 20 characters |
Qerko doesn’t check the refund against the original payment (amount, currency, earlier refunds or a reversal). That is up to the payment processor. An unknown refundId isn’t rejected right away: the refund starts and then ends as failed with reason: declined.
Response: type: started with the refund (status: pending), or unknownTerminal, notStarted or one of the start errors.
Poll POST /get-terminal-refund-status with the refund’s transactionId:
{
"type": "found",
"refund": {
"status": "approved",
"transactionId": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
"referenceId": "CN-2026-000017",
"amount": "50.00",
"currency": "CZK",
"createdAt": "2026-10-15T11:02:10.000Z",
"finishedAt": "2026-10-15T11:02:18.000Z",
"authorizationCode": "654321",
"card": {
"brand": "VISA",
"maskedPan": "************8902"
},
"receipt": {
"merchantId": "000123456",
"terminalId": "A1B2C3D4",
"sequenceNumber": "000057",
"terminalDateTime": "2026-10-15T11:02:17.000Z"
}
}
}status | Meaning |
|---|---|
pending | The refund is running |
approved | The amount was returned to the card, and the response contains the data for the refund receipt |
failed | Nothing was returned (reason: declined or cancelled) |
unknown | The result could not be determined and has to be checked on the terminal |
Step 6: Closings
A closing (end-of-day, batch close) closes the terminal’s current batch and returns its totals. Run it, for example, at the end of the day for each terminal.
POST /create-terminal-closing
Request
{
"terminalId": "A1B2C3D4",
"transactionId": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"
}Response: type: started with the closing (status: pending), or unknownTerminal, notStarted or one of the start errors.
Poll POST /get-terminal-closing-status with the closing’s transactionId:
{
"type": "found",
"closing": {
"status": "completed",
"transactionId": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"terminalId": "A1B2C3D4",
"createdAt": "2026-10-14T22:00:01.000Z",
"finishedAt": "2026-10-14T22:00:09.000Z",
"batchNumber": 128,
"shiftNumber": 1,
"terminalDateTime": "2026-10-14T22:00:08.000Z",
"payments": { "count": 57, "amount": "18450.00" },
"refunds": { "count": 1, "amount": "50.00" },
"adjustments": { "count": 0, "amount": "0.00" }
}
}status | Meaning |
|---|---|
pending | The closing is running |
completed | The batch was closed. The response contains the batch number, the shift number and the totals (count and sum) of payments, refunds and adjustments in the terminal’s currency. |
failed | The batch was not closed (reason: declined or cancelled) |
unknown | The result could not be determined and has to be checked on the terminal |
Errors when starting an operation
All create-terminal-* methods (and /get-terminal-status) can end with one of the following outcomes instead of started. Each has a message with a short description.
type | Meaning | What to do |
|---|---|---|
unknownTerminal | The terminal doesn’t exist, isn’t enabled, or belongs to another company | Check the terminal in /get-terminals |
terminalNotActivated | The terminal isn’t activated at the payment processor | The terminal has to be set up, and retrying doesn’t help. Contact Qerko support. |
terminalBusy | The terminal is used by another cash register, or an earlier operation on it is still running | Wait for the running operation to finish, then start again |
terminalNotConnected | The terminal is offline: switched off, no connection, or its payment app isn’t running | Ask the staff to check the terminal, then retry |
busy | The terminal is processing another operation right now | Retry in a few seconds |
duplicateTransaction | This transactionId was already used | Generate a new transactionId. If you were retrying, check the state of the original operation first (see Recommendations). |
upstreamTimeout | The payment processor didn’t answer in time, and it isn’t known whether the operation started | Check the terminal before retrying. A started payment or refund that nobody confirms is reversed automatically. |
upstreamError | The request to the payment processor failed | Retry later |
notStarted | The payment processor refused to start the operation | See cause.code and cause.message for the processor’s reason |
Recommendations
- Generate a new
transactionIdfor every operation, and save it in your cash register before you call thecreate-terminal-*method. - Don’t blindly repeat a start when its result is unclear. If the call fails or times out on your side (network error, …), call the matching
get-terminal-*-statuswith the sametransactionIdfirst:found: the operation exists, so continue polling it.unknownTransaction: the operation was not started, so you can start it again.
- Poll the status once per second while
statusispending, and stop when the status is final. Showstepto the staff so they know what is happening on the terminal. - Only one operation runs on a terminal at a time. Wait until the previous payment, reversal, refund or closing reaches its final status before you start another one on the same terminal.
- Your cash register doesn’t have to stay online. Qerko finishes a started operation even if your cash register stops polling or restarts. After a restart, look up unfinished operations by their saved
transactionId. - Use
chargedAmountfor the bill, not the requestedamount, because the customer may have added a tip on the terminal. - Store
receipt.refundIdof every approved payment, because it is needed for a later refund. - Handle
unknowncarefully. It is rare, but when it happens, the staff must check the result on the terminal (in its transaction history) before they treat the bill as paid or unpaid.
Methods at a glance
| Method | Purpose |
|---|---|
/get-terminals | Company, its locations and terminals |
/get-terminal-status | Is the terminal connected and does it reach the authorization server? |
/create-terminal-payment | Start a payment |
/get-terminal-payment-status | State and result of a payment |
/create-terminal-reversal | Reverse an approved payment in full |
/get-terminal-reversal-status | State and result of a reversal |
/create-terminal-refund | Refund (a part of) a payment back to its card |
/get-terminal-refund-status | State and result of a refund |
/create-terminal-closing | Close the terminal batch |
/get-terminal-closing-status | State and totals of a closing |
Need help? Contact techsupport@qerko.com for assistance.