Reference
Receipt JSON
{
"mode": "image", // optional: "image" (default) or "text"
"dots": 576, // optional: printable width in dots. Default: printer profile, then config.
"width": 48, // optional: characters per line (text mode), and the unit for column widths
"font": "sans", // image mode: "sans" (default) or "mono"
"fontSize": 24, // image mode: pixel height of normal text
"codepage": "cp437", // text mode: cp437 (default), cp850, cp858 (has €), cp866 (Cyrillic), cp1252
"items": [ ...blocks ]
}
Image mode vs text mode
| image (default) | text | |
|---|---|---|
| How | Agent draws the receipt with TTF fonts at the printer's width and sends a raster (GS v 0). Cut/drawer are sent as commands | Printer's built-in font via ESC/POS text commands |
| Looks the same on all brands | ✅ | ❌ (fonts, column counts, codepages differ) |
| Languages | Anything your font covers (set fontFile in the config, e.g. a Noto font for Cyrillic/Greek/CJK). Scripts that need shaping (Arabic, Urdu, Hindi) should be drawn in the browser and sent as an image block | Limited to the printer's codepages |
| Data per receipt | ~10–40 KB | ~0.2–1 KB |
| Best for | USB / network printers, consistent branding | Bluetooth/serial links, very old printers |
In image mode, size scales text (1–4), and column width values are characters of width (default dots / 12), converted to pixels.
| Block | Fields | Notes |
|---|---|---|
text | value, align, bold, underline, invert, size (1–4) | Word-wraps to the line width (divided by size). \n starts a new line. |
row | left, right, bold | Item/price pair: right side is right-aligned, left side wraps. |
columns | columns: [{text, width?, align?}], bold | width in characters; columns without a width share the rest. Cells wrap independently. |
line | char (default -) | Full-width divider. |
feed | lines | Blank lines. |
qr | value, size (1–16, default 6), align | Native printer QR (GS ( k, model 2, ECC M). |
barcode | value, format (CODE128 default, EAN13, EAN8, UPCA, CODE39), height, hri, align | EAN/UPC values are length/digit checked. |
image | data (base64 or data: URL of PNG/JPEG/GIF), align, size (max width in dots), threshold, dither | Scaled to paper width, Floyd–Steinberg dithered, sent as GS v 0 raster in 128-row bands. Transparency prints white. |
cut | partial | Feeds 3 lines, then cuts. |
drawer | pin (2 or 5) | Kicks the cash drawer connected to the printer. |
raw | data (base64) | Inject any bytes (vendor-specific commands). |
Characters that the selected codepage cannot represent print as ?. Use cp858 for €, cp866 for Cyrillic. CJK/Arabic/Urdu text should be sent as an image (render it to a canvas in the browser, then call Thermalink.imageToDataURL(canvas.toDataURL())).
HTTP API (agent)
Base URL http://127.0.0.1:17777. All bodies are JSON.
| Method & path | Body | Response |
|---|---|---|
GET /v1/health | none | {ok, name, version}. Open to all, used to detect the agent. |
GET /v1/printers | none | {ok, printers: [...os names], aliases: {name: target}, default} |
POST /v1/preview | {receipt, printer?} | {ok, mode, png, text, bytes}. png is a data URL of exactly what will print (image mode), using that printer's profile |
POST /v1/print | {receipt} or {raw: base64}, plus printer?, copies? (1–20) | {ok, printer, bytes, copies, ms} |
Errors: {ok:false, error} with status 400 (bad document), 401 (missing/wrong key), 403 (origin or host not allowed), 413 (body > 8 MB), 502 (printer unreachable/failed).
printer may be an alias, an OS printer name, tcp://host[:port] or file://path (writes bytes to a file, useful for tests). Empty means the default printer.
Jobs to the same printer are serialised, so two tabs printing at once never interleave.
Security model
- The agent listens on 127.0.0.1 only. It is never reachable from the network.
- Browser calls must come from an origin you allowed (
thermalink allow). The browser sets theOriginheader, and pages cannot fake it. - Non-browser calls (scripts, curl, a local backend) must send
X-Thermalink-Key: <apiKey>from the config file. - Requests whose
Hostis not a loopback name are rejected (protects against DNS rebinding). https://sites can reach the agent: the SDK setstargetAddressSpace: 'loopback'automatically and the agent answers the older Private Network Access preflight. Chrome 142+ and Firefox 153+ also ask the user once per site for permission;tl.status()andtl.permission()tell you where that stands (see Troubleshooting).
Config file
thermalink config prints the path and contents. Default locations: %APPDATA%\Thermalink\config.json (Windows), ~/Library/Application Support/Thermalink/config.json (macOS), ~/.config/Thermalink/config.json (Linux). Override with the env var THERMALINK_CONFIG.
{
"port": 17777,
"allowedOrigins": ["https://pos.yourapp.com"],
"apiKey": "generated-on-first-run",
"printers": {
"receipt": { "target": "EPSON TM-T20III Receipt", "dots": 576 },
"kitchen": "tcp://192.168.1.50:9100",
"slip": { "target": "POS-58", "dots": 384, "cols": 32, "mode": "text" }
},
"defaultPrinter": "receipt",
"mode": "image",
"paperWidth": 48,
"paperDots": 576,
"fontFile": "",
"fontBoldFile": "",
"timeoutSeconds": 10
}
Restart the agent after editing the file by hand. The CLI commands (allow, alias, default) also need a restart of a running agent to take effect.
More docs: Quick start · Troubleshooting · Compatibility