POST /api/print/thermal
Endpoint:
POST /api/print/thermal
Auth Scope: Profile Protected (verify_operational_profile_access)
Target Service: Local DaemonX Bridge Daemon (127.0.0.1:9123)
Introduced In: v1.0.0
Last Updated: v1.0.0
Average Latency: < 15ms
1. Description & Purpose
POST /api/print/thermal sends binary ESC/POS command sequences directly to physical receipt printers over USB, LAN, or COM ports. It operates with zero browser print dialogs, zero rasterization lag, and sub-15ms execution time.
Core Capabilities:
- Instant Cut: Sends standard full/partial knife cut commands (
GS V B \x00). - Simultaneous Cash Drawer Kick: Fires an ESC/POS pulse pin (
ESC p \x00 \x19 \xFA) immediately before the receipt prints. - Auto-Formatting (58mm vs 80mm): Automatically calculates column widths (32 columns for 58mm, 42 columns for 80mm) and aligns item prices.
- Sandbox Watermark Injection: In Sandbox environments, DaemonX automatically prepends and appends physical test receipt banners to prevent unauthorized live commercial use.
- Three Input Formats: Supports Structured Lines (recommended for AI and web apps), Raw Base64 (for logos, barcodes, and custom ESC/POS bytes), and Plain Text.
2. Authentication & Required Headers
| Header Name | Type | Required | Description | Example |
|---|---|---|---|---|
Content-Type | string | Yes | application/json | application/json |
Origin | string | Yes | Web POS browser origin. Must be whitelisted in profile. | https://pos.yourdomain.com |
X-Bridge-Counter-Id | string (UUID) | Yes | Target counter UUID registered on this workstation. | d402ddec-ecf7-4b39-b57a-86ce6008df05 |
X-Bridge-Profile-Secret | string | Yes | Secret issued during counter pairing (dxp_...). | dxp_202b541b2a72ecc32867e0f62b26b... |
3. TypeScript Contracts
export type ThermalLineType =
| 'header' // Centered, double-height, double-width bold text
| 'divider' // Automatic dashed separator line (32 or 42 dashes)
| 'item' // Line item with bold product name, quantity, and right-aligned price
| 'total' // Summary row (Subtotal, Tax, Total) with bold aligned label and value
| 'text'; // Freeform text with custom alignment
export interface ThermalLine {
type: ThermalLineType;
text?: string;
label?: string;
value?: string | number;
name?: string;
qty?: string | number;
price?: string | number;
align?: 'left' | 'center' | 'right';
}
export interface ThermalPrintRequest {
/** Exact Windows printer name (from GET /api/printers) */
printer_name: string;
/** Paper width: '80mm' (42 cols) or '58mm' (32 cols). Defaults to '80mm' */
format?: '80mm' | '58mm';
/** High-level structured receipt lines (Recommended) */
lines?: ThermalLine[];
/** Pre-encoded raw ESC/POS binary string in Base64 (for graphics/bitmaps) */
raw_base64?: string;
/** Simple plain text fallback (encoded automatically as CP437) */
text?: string;
/** Automatically trigger printer paper cutter after payload. Default: true */
cut?: boolean;
/** Pulse pin 2 to open connected RJ11 cash drawer before print. Default: false */
kick_drawer?: boolean;
}
export interface ThermalPrintResponse {
success: boolean;
counter_id: string;
printer: string;
bytes: number;
}
4. Parameter Matrix
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
printer_name | string | Yes | - | Name of target Windows printer as reported by GET /api/printers. |
format | string | No | "80mm" | "80mm" (42 columns) or "58mm" (32 columns). |
lines | ThermalLine[] | Conditional | null | Structured line objects. Required if raw_base64 or text is not provided. |
lines[].type | string | Yes | - | "header", "divider", "item", "total", or "text". |
lines[].name | string | Conditional | null | Item title (used with type: "item"). |
lines[].qty | number | string | No | null | Item quantity (e.g. 2 or "1.5 kg"). |
lines[].price | number | string | No | null | Item line total (e.g. 1200.00 or "$12.00"). |
lines[].label | string | Conditional | null | Summary label (e.g. "SUBTOTAL", "DISCOUNT"). |
lines[].value | number | string | No | null | Summary amount (e.g. 2400.00). |
lines[].align | string | No | "left" | "left", "center", or "right". |
raw_base64 | string | Conditional | null | Base64-encoded ESC/POS bytes. Bypasses structured formatting. |
text | string | Conditional | null | Plain text with newlines. |
cut | boolean | No | true | Appends paper feed and partial cut command. |
kick_drawer | boolean | No | false | Fires 50ms cash drawer kick pulse before receipt prints. |
5. Real-World Request & Response Examples
Example A: Complete Retail Receipt with Drawer Kick & Cut
Endpoint: POST http://127.0.0.1:9123/api/print/thermal
{
"printer_name": "EPSON TM-T82III Receipt",
"format": "80mm",
"kick_drawer": true,
"cut": true,
"lines": [
{ "type": "header", "text": "STUDIO MILAN" },
{ "type": "text", "text": "No. 45 Galle Road, Colombo 03", "align": "center" },
{ "type": "text", "text": "Tel: +94 11 234 5678", "align": "center" },
{ "type": "divider" },
{ "type": "text", "text": "Invoice: #INV-2026-0042", "align": "left" },
{ "type": "text", "text": "Cashier: Kasun Perera", "align": "left" },
{ "type": "text", "text": "Date: 2026-10-05 03:00 PM", "align": "left" },
{ "type": "divider" },
{ "type": "item", "name": "Linen Casual Shirt - Navy (M)", "qty": 1, "price": 4500.00 },
{ "type": "item", "name": "Slim Fit Chino Pants - Khaki", "qty": 2, "price": 7800.00 },
{ "type": "item", "name": "Leather Dress Belt", "qty": 1, "price": 2200.00 },
{ "type": "divider" },
{ "type": "total", "label": "Subtotal", "value": 14500.00 },
{ "type": "total", "label": "Loyalty Discount (10%)", "value": -1450.00 },
{ "type": "total", "label": "VAT (0%)", "value": 0.00 },
{ "type": "divider" },
{ "type": "total", "label": "GRAND TOTAL", "value": "LKR 13,050.00" },
{ "type": "total", "label": "Cash Tendered", "value": "LKR 15,000.00" },
{ "type": "total", "label": "Change Due", "value": "LKR 1,950.00" },
{ "type": "divider" },
{ "type": "text", "text": "Thank you for shopping with us!", "align": "center" },
{ "type": "text", "text": "Exchange possible within 7 days with bill.", "align": "center" },
{ "type": "text", "text": "Powered by Nyxzie Commerce", "align": "center" }
]
}
Successful Response (200 OK)
{
"success": true,
"counter_id": "d402ddec-ecf7-4b39-b57a-86ce6008df05",
"printer": "EPSON TM-T82III Receipt",
"bytes": 542
}
Example B: Offline Allowance Exhausted (403 Forbidden)
{
"detail": {
"error": "DAEMONX_OFFLINE_ALLOWANCE_EXHAUSTED",
"message": "Emergency offline allowance exhausted. Reconnect this counter to continue.",
"counter_id": "d402ddec-ecf7-4b39-b57a-86ce6008df05",
"offline_remaining": 0,
"reconnect_required": true,
"gate_reason": "OFFLINE_ALLOWANCE_EXHAUSTED",
"operation": "print_thermal"
}
}
6. Sandbox Watermark Behavior
If the counter was activated using a Sandbox API Key (nyx_daemonx_sandbox_*), DaemonX enforces physical watermark injection. The printer will physically print:
==========================================
*** DAEMONX TEST RECEIPT ***
*** SANDBOX ENVIRONMENT ***
NOT A VALID TAX INVOICE
==========================================
[ ... Cashier receipt contents ... ]
==========================================
*** TEST RECEIPT ***
UNAUTHORIZED FOR COMMERCIAL SALE
==========================================
In live environments (nyx_daemonx_live_*), watermark_enforced is false and zero watermark banners are printed.
7. Integration Snippets
cURL
curl -X POST "http://127.0.0.1:9123/api/print/thermal" \
-H "Content-Type: application/json" \
-H "X-Bridge-Counter-Id: d402ddec-ecf7-4b39-b57a-86ce6008df05" \
-H "X-Bridge-Profile-Secret: dxp_202b541b2a72ecc32867e0f62b26b07be38024210e30af90" \
-H "Origin: https://pos.yourdomain.com" \
-d '{
"printer_name": "EPSON TM-T82III Receipt",
"format": "80mm",
"cut": true,
"lines": [
{ "type": "header", "text": "QUICK TEST" },
{ "type": "text", "text": "Hello from DaemonX!", "align": "center" }
]
}'
TypeScript Client Function
export async function printThermalReceipt(
request: ThermalPrintRequest,
counterId: string,
profileSecret: string
): Promise<ThermalPrintResponse> {
const res = await fetch('http://127.0.0.1:9123/api/print/thermal', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Bridge-Counter-Id': counterId,
'X-Bridge-Profile-Secret': profileSecret,
},
body: JSON.stringify(request),
});
if (!res.ok) {
const errorPayload = await res.json().catch(() => null);
const detail = errorPayload?.detail;
if (res.status === 403 && detail?.reconnect_required) {
throw new Error(`OFFLINE_LOCKED: ${detail.message}`);
}
throw new Error(detail?.message || `Print failed with HTTP ${res.status}`);
}
return (await res.json()) as ThermalPrintResponse;
}