# `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

```typescript
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`

```json
{
  "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)
```json
{
  "success": true,
  "counter_id": "d402ddec-ecf7-4b39-b57a-86ce6008df05",
  "printer": "EPSON TM-T82III Receipt",
  "bytes": 542
}
```

---

### Example B: Offline Allowance Exhausted (403 Forbidden)
```json
{
  "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:

```text
==========================================
       *** 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
```bash
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
```typescript
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;
}
```
