Print Thermal Receipt

Send silent ESC/POS thermal receipt jobs through the local bridge.

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 NameTypeRequiredDescriptionExample
Content-TypestringYesapplication/jsonapplication/json
OriginstringYesWeb POS browser origin. Must be whitelisted in profile.https://pos.yourdomain.com
X-Bridge-Counter-Idstring (UUID)YesTarget counter UUID registered on this workstation.d402ddec-ecf7-4b39-b57a-86ce6008df05
X-Bridge-Profile-SecretstringYesSecret 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

ParameterTypeRequiredDefaultDescription
printer_namestringYes-Name of target Windows printer as reported by GET /api/printers.
formatstringNo"80mm""80mm" (42 columns) or "58mm" (32 columns).
linesThermalLine[]ConditionalnullStructured line objects. Required if raw_base64 or text is not provided.
lines[].typestringYes-"header", "divider", "item", "total", or "text".
lines[].namestringConditionalnullItem title (used with type: "item").
lines[].qtynumber | stringNonullItem quantity (e.g. 2 or "1.5 kg").
lines[].pricenumber | stringNonullItem line total (e.g. 1200.00 or "$12.00").
lines[].labelstringConditionalnullSummary label (e.g. "SUBTOTAL", "DISCOUNT").
lines[].valuenumber | stringNonullSummary amount (e.g. 2400.00).
lines[].alignstringNo"left""left", "center", or "right".
raw_base64stringConditionalnullBase64-encoded ESC/POS bytes. Bypasses structured formatting.
textstringConditionalnullPlain text with newlines.
cutbooleanNotrueAppends paper feed and partial cut command.
kick_drawerbooleanNofalseFires 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;
}