Printing on SmartPOS terminal

The Print API gives you full control over the thermal printer built into Kushki ONE terminals. Print any content from your POS system — before, during or after a transaction, or with no transaction at all — without installing drivers, SDKs or configuring hardware on your side.

Requirements

How it works

The print cycle has two parts: a sync request that queues the job, and an async notification that tells you when it finished.

Kushki ONE Print API sequence diagram

  1. Your POS system builds a commands array with the receipt layout.
  2. You send the job creation request. The terminal answers 202 Accepted right away.
  3. Printing runs asynchronously on the hardware.
  4. You learn the final result over webhook or by querying the status.

The request and response structure is identical across topologies. The only thing that changes is the base URL.

TopologyBase URL
Local Network (LAN / Wi-Fi)http://{TERMINAL_IP}:6868/terminal/v1
Cloud (Internet) — UAThttps://uat-cloudt.kushkipagos.com/terminal/v1/{terminalSerial}/sync
Cloud (Internet) — Productionhttps://cloudt.kushkipagos.com/terminal/v1/{terminalSerial}/sync

Authentication

Include these headers with every request, the same as in the Payment API:

HeaderValue
AuthorizationHMAC-SHA256 signature of the request body, Base64-encoded, using your Business-Code as the key
timestampUnix timestamp in milliseconds

Use cases

The API is not limited to payment receipts. Any content your business needs to hand over on paper can be triggered from your POS system.

Use caseDescription
Payment receiptDirect sale, pre-authorization capture, refund or void
Discount couponA code for the customer’s next purchase
QR codeWi-Fi password, loyalty link, digital receipt, product information
Loyalty and promotionsPoint balance, reward tiers, special offers
Pre-check or order summaryKitchen ticket or table summary before the final charge
Reversal recordA printed record of a cancellation or refund
ReprintPrint a previous receipt again with the same printJobId
Free contentText, image, QR or barcode, with no transaction involved

Anatomy of a receipt

Each visual section of the receipt maps to a command type inside the commands array.

Anatomy of a receipt mapped to Print API commands

Request structure

FieldTypeRequiredDescription
commandsarrayOrdered list of print commands
printJobIdstringIdempotency key. If you leave it out, a UUID is generated
externalReferencestringYour own free-form reference, for example Table-14
webhookUrlstringURL that will receive the result when the job finishes
skipIfBusybooleanWith true, returns 409 immediately if the queue is busy. Defaults to false

Command types

TypeDescription
textA line of text with size, alignment, bold, italic and underline
columnsA multi-column row with proportional widths, ideal for product and price
dividerA full-width separator line: SOLID, DOTTED or EMPTY
feedAdvances the paper N blank lines
spaceInserts precise vertical space in pixels
cutTriggers the cutter. Safely ignored on terminals without one
imagePrints a PNG or JPG image in Base64. Use algorithm: BINARIZATION for logos
qrGenerates a QR code on the printer hardware
barcodeGenerates a CODE128 barcode on the hardware

Full example

This request builds a receipt with a logo, header, line items, total, QR code and an automatic cut.

{
"printJobId": "TICKET-190209",
"externalReference": "Table-14",
"webhookUrl": "https://api.yourbusiness.com/webhook/print-events",
"skipIfBusy": false,
"commands": [
{ "type": "image", "base64Image": "iVBORw0KGgoAAAANSUhEUg...", "align": "CENTER", "width": 300, "algorithm": "BINARIZATION" },
{ "type": "text", "text": "EL BUEN SABOR RESTAURANT\n", "align": "CENTER", "size": 32, "bold": true },
{ "type": "text", "text": "Tax ID: 900.123.456-7\n", "align": "CENTER", "size": 22 },
{ "type": "divider", "dividerType": "DOTTED" },
{ "type": "columns", "columns": [
{ "text": "2x Burger Combo", "weight": 2, "align": "LEFT" },
{ "text": "30,000.00 COP", "weight": 1, "align": "RIGHT" } ] },
{ "type": "columns", "columns": [
{ "text": "1x Fresh Juice", "weight": 2, "align": "LEFT" },
{ "text": "8,000.00 COP", "weight": 1, "align": "RIGHT" } ] },
{ "type": "divider", "dividerType": "SOLID" },
{ "type": "columns", "columns": [
{ "text": "TOTAL", "weight": 2, "align": "LEFT" },
{ "text": "38,000.00 COP", "weight": 1, "align": "RIGHT" } ] },
{ "type": "qr", "content": "https://yourbusiness.com/receipt/TICKET-190209", "dotSize": 6, "errorLevel": "M", "align": "CENTER" },
{ "type": "feed", "lines": 3 },
{ "type": "cut" }
]
}

Send the request from your back-end:

  • Javascript
  • Python
const res = await fetch(`${BASE_URL}/print`, {
method: "POST",
headers: buildHeaders(payload), // Authorization + timestamp
body: JSON.stringify(payload),
});
const job = await res.json();
console.log(res.status, job.printJobId, job.status);
// 202 TICKET-190209 PENDING
import requests
res = requests.post(f"{BASE_URL}/print",
headers=build_headers(payload), json=payload)
job = res.json()
print(res.status_code, job["printJobId"], job["status"])
# 202 TICKET-190209 PENDING

The terminal answers immediately:

{
"printJobId": "TICKET-190209",
"status": "PENDING",
"message": "Impresión encolada correctamente"
}
CodeMeaning
202 AcceptedJob queued. Returns the printJobId and PENDING status
400 Bad RequestMalformed payload or unknown enumerated value
409 ConflictA job is already PENDING or IN_PROGRESS

Job result

You have two mechanisms to learn the final result. Use one or both in parallel.

Option A: webhook

If you sent a webhookUrl when queueing, the terminal makes a POST to that URL when the job turns COMPLETED or FAILED.

Successful job

{
"printJobId": "TICKET-190209",
"status": "COMPLETED",
"externalReference": "Table-14"
}

Hardware failure

{
"printJobId": "TICKET-190209",
"status": "FAILED",
"externalReference": "Table-14",
"errorCode": "OUT_OF_PAPER",
"errorMessage": "La impresora está sin papel."
}

Option B: query the status

Use this when your system cannot receive inbound connections from the terminal, or as a webhook fallback.

Local Network

GET /terminal/v1/print_job?print_job_id=TICKET-190209

Cloud

POST /terminal/v1/{terminalSerial}/sync/print_job
{
"print_job_id": "TICKET-190209"
}

Poll every 2 or 3 seconds and stop once status is COMPLETED or FAILED.

{
"printJobId": "TICKET-190209",
"status": "COMPLETED"
}

Job statuses

statusDescription
PENDINGQueued, waiting its turn
IN_PROGRESSThe driver is sending commands to the printer
COMPLETEDReceipt printed and cut successfully
FAILEDPhysical error during printing. Check errorCode

Hardware error codes

The possible values of errorCode are OUT_OF_PAPER, COVER_OPEN, COVER_INCOMPLETE, PAPER_JAM, BUSY, PRINTER_HOT, MOTOR_HOT, CUTTER_ERROR, OFFLINE and UNKNOWN_ERROR.

Best practices

  • Send your own printJobId so you can reprint the same receipt and discard duplicates in your system.
  • Use externalReference to tie the receipt to your order, table or invoice.
  • Answer the webhook with 2xx before processing it. A slow endpoint means you lose the notification.
  • Treat FAILED as an operational condition, not an integration error: show the errorMessage to the operator so they can fix the physical problem.
  • Send images already binarized and at the right width. A heavy logo lengthens print time.
Accept payments with Kushki ONE

Process card-present payments and pair them with printing the receipt.

Printer errors

Look up the cause and the recommended action for every hardware error code.