# Virtual Printer - complete documentation Source: https://virtual-printer.online/docs. Generated on request, so this file is never stale. --- # About Virtual Printer **Virtual Printer** is a hosted thermal printer emulator that lets developers test receipt and label printing without physical hardware. ## What is Virtual Printer? Virtual Printer speaks ESC/POS and EPL over a network socket, the way a real receipt or label printer does. Instead of purchasing physical network printers, you can create virtual thermal printers that emulate real hardware for testing POS printing applications. Send print commands over a raw TCP socket exactly as you would to a network printer — the same JetDirect/AppSocket stream physical thermal printers accept on port **9100** — and view the rendered documents in real-time through our web interface. Test ESC/POS and EPL commands without requiring physical devices. **The hosted service listens on real, public raw TCP ports.** virtual-printer.online is always online, and every printer you create is given its own permanent port in the 9100 block. There is no HTTP wrapper to work around, no browser extension, and nothing to install locally: open a socket to `virtual-printer.online:`, write your ESC/POS bytes, close the connection, and the receipt appears in the browser. Create printers with different paper widths and configurations to test how your documents render across various printer models and hardware specifications. Perfect for developers building point-of-sale systems, receipt/label printing APIs, or any application requiring thermal printer testing. ## Key Features ### Public Raw TCP Port — Always Online Each virtual printer gets its own raw TCP listener on the public internet, the same way a network thermal printer listens on port 9100. Ports are assigned from the 9100 block and belong to the printer permanently, so your POS application can keep a fixed `host:port` in its configuration. ### Real-Time Document Preview View print jobs as they're processed with accurate rendering of text, images, and formatting. ### Printer Protocol Emulation Virtual printer simulator supporting printer command languages: - **ESC/POS Emulator** - Test receipt printer commands - **EPL Emulator** - Test Eltron label printer commands - **TSPL Emulator** - (In development) TSC printer command testing ESC/POS thermal printer emulation includes: - Text formatting (bold, underline, alignment) - Character sizing and spacing - Image printing - Paper cutting commands ### Multi-Printer Management Create and manage multiple virtual printers per workspace with individual configurations. ### Workspace Sharing Share your entire workspace across devices and team members, enabling collaborative development and document review. ### Buffer Emulation Simulate real printer behavior with configurable buffer capacity and drain rates. ## Perfect For - **POS Development**: Test receipt printing without physical thermal printers - **API Testing**: Develop and test printing integrations without hardware - **ESC/POS Development**: Debug receipt printer commands and formatting - **Label Printer Testing**: Test EPL and other label printing protocols - **Mock Printer Testing**: Simulate network printers for development environments - **Remote Development**: Share virtual printers with distributed teams - **Demo Environments**: Showcase printing functionality without hardware setup - **Controlled Conditions**: Reproduce, on demand and through the API, what is awkward to stage on real hardware — a full input buffer, a slow drain rate, paper out mid-receipt, an open cover, an offline printer — and check how your POS application behaves ## Where to go next - [Quick start](/docs/start/quick-start) — workspace, printer, and first receipt in about five minutes - [Raw TCP port 9100](/docs/start/raw-tcp-port-9100) — what the port is, how it is assigned, how long it lasts - [How it works](/docs/concepts/how-it-works) — what happens between your socket and the rendered receipt - [Choosing a test printer](/docs/concepts/choosing-a-test-printer) — whether this is the right tool for your case - [CI/CD and automated tests](/docs/ci-cd) — driving the printer from test code ## Support Questions or issues? See the [FAQ](/docs/faq), the [troubleshooting guide](/docs/troubleshooting), or email [support@virtual-printer.online](mailto:support@virtual-printer.online). --- # Quick Start Learn how to test receipt and label printing without a physical printer using Virtual Printer's online emulator. ## Quick Start: Virtual Receipt Printer Setup 1. **Create a Workspace**: Click "Create or Access Workspace" and enter a workspace nickname 2. **Save Your Token**: Store your workspace token securely for future access 3. **Add a Virtual Printer**: Click "New Printer" and configure your ESC/POS emulator 4. **Get the Address**: Note the raw TCP address — `virtual-printer.online` plus the port assigned to your printer from the 9100 block (e.g. `virtual-printer.online:9107`) 5. **Test Print Commands**: Point your application to the virtual printer address ## Connecting to the Virtual Thermal Printer Configure your POS application to send ESC/POS commands to the virtual receipt printer over a plain socket — the same raw TCP (JetDirect / AppSocket) stream a physical network printer accepts on port 9100. No HTTP request, no browser, no local bridge or container: ``` Host: virtual-printer.online (or your server IP if self-hosted) Port: 9107 (your printer's own port from the 9100 block - shown on the printer card) Protocol: Raw TCP (port 9100 style), not HTTP ``` The listener is public and always online, so the address works from anywhere — your laptop, a CI runner, or a POS terminal in the field. ## Next - [Which port do I get, and why does it start at 9100?](/docs/start/raw-tcp-port-9100) - [Import a captured dump](/docs/start/import-a-dump) if you already have the bytes and no application to print them - [Code samples](/docs/setup/code-samples) for C#, C++, Node.js and Python - [Driving the printer from autotests](/docs/ci-cd) - [Security guidelines](/docs/policies/security) before you send anything real --- # Raw TCP Port 9100 Network thermal printers do not speak HTTP. They listen on a plain TCP socket — usually port **9100**, the convention known as JetDirect or AppSocket — and print whatever bytes arrive until the sender closes the connection. There is no request, no response, and no framing: the socket *is* the protocol. virtual-printer.online works the same way, on the public internet. ## Yes, there is a real public port Every printer you create gets its own always-online TCP listener on `virtual-printer.online`, reachable from anywhere. Point your POS software at `virtual-printer.online:`, write ESC/POS or EPL bytes, close the connection, and the rendered receipt appears in the browser. That means: - **Nothing to install.** No Docker container, no local bridge, no browser extension, no agent. - **No HTTP wrapper in the print path.** You do not POST your payload anywhere; the code under test opens a socket, exactly as it does against real hardware. - **The browser is optional.** It does not need to be open while you print — parsing and rendering happen on the server. The HTTP/REST API exists as well, but for managing printers and reading documents back. Printing itself is always raw TCP. ## Which port do I get? Network thermal printers listen for raw TCP jobs on port **9100**, and virtual-printer.online hands out its ports from that same block. Port 9100 itself is the anchor of the range and is never assigned; printers are numbered upward from it — 9101, 9102, 9103, and so on. Ports taken by printers registered before yours stay taken, so a printer created today is assigned the next free port above them. The assigned port is **permanent**: it belongs to that printer until you delete the printer, or until the workspace expires after a year with nobody opening it or reading a document from it. So you can put it in your POS configuration and leave it there. Assignment is automatic — you cannot request a specific port, and in particular you cannot claim 9100 itself. ## Finding your port The port is printed on the printer's card in the web interface, and returned as `settings.tcpListenPort` by the API when you create or fetch a printer, alongside `settings.publicHost`. ## Checking that the port is reachable Before wiring up your application, confirm the path works from where your code runs: ```bash # Linux / macOS printf 'Hello ' | nc virtual-printer.online 9411 # Windows PowerShell - connectivity only, sends nothing Test-NetConnection virtual-printer.online -Port 9411 ``` Substitute your own port. If the connection is refused, start with [Connection refused](/docs/troubleshooting) — the usual causes are a stopped printer or an outbound firewall that only allows 80 and 443. ## Security Raw TCP is unencrypted, by definition of what a printer socket is. Anything you print to the hosted service travels in the clear — see [Security guidelines](/docs/policies/security) before sending anything that resembles a real receipt. --- # Import a Captured Dump Printing over a socket is the normal path, but it is not the only one. If you already have the bytes — a capture from a real printer, a payload attached to a bug report, a fixture from your test suite — you can feed them straight to a printer and look at the result. No socket, no application, no code. This is the fastest way to answer "what does this dump actually draw?". ## In the web interface 1. Select the printer, or create one with the paper width you want to render against. 2. Open the **Operations** panel on the right and click **Import Doc**. 3. Either **drop a file** (or pick one), or **paste the bytes** into the field - a hex dump or base64, whichever you have. 4. The bytes go through the same parser a printed job goes through, and the document appears in the list exactly as if it had arrived over TCP — including any parse errors. Make sure the printer's protocol matches the dump: ESC/POS data on an EPL printer renders nothing useful. See [Printer protocols](/docs/protocols). ## Over the API ```bash # base64 curl -sf -X POST "https://virtual-printer.online/api/printers/$PRINTER_ID/documents/import" \ -H "Authorization: Bearer $ACCESS" \ -H 'Content-Type: application/json' \ -d "{\"Data\": \"$(base64 -w0 receipt.bin)\"}" # a hex dump, with the format stated curl -sf -X POST "https://virtual-printer.online/api/printers/$PRINTER_ID/documents/import" \ -H "Authorization: Bearer $ACCESS" \ -H 'Content-Type: application/json' \ -d '{"Data": "1B 40 48 65 6C 6C 6F 0A", "Format": "Hex"}' ``` `Data` carries the bytes and `Format` is `"Hex"` or `"Base64"`. **`Format` is optional** — leave it out and the format is detected — but see [Telling the two apart](#telling-the-two-apart) before relying on that. The response is **204** on success and **400** with the reason otherwise. Then read the parsed document back with `GET /api/printers/{id}/documents/canvas` — see [Asserting on a receipt](/docs/ci-cd/assertions). In a test suite this turns a byte dump into a regression test: keep the capture as a fixture, import it, assert on the elements. It is also how a support case with an attached dump becomes something you can look at, without reproducing whatever produced it. ## Accepted formats | Input | Accepted | |---|---| | A binary file (`.bin`, `.prn`, raw capture) | Yes — drop it or select it | | A hex dump (`1B 40 48 65 6C 6C 6F 0A`) | Yes — paste it, or send it with `"Format": "Hex"` | | Base64 text | Yes — paste it, or send it in `Data` | Whitespace is ignored in both text forms, so a dump copied out of a debugger, wrapped across lines or run together all work. Escaped forms — `\x1B`, `0x1B, 0x40` — are not accepted; strip them to plain hex digits first. ## Telling the two apart The two formats overlap, and the overlap is a trap worth understanding. Every hex digit is also a base64 character. A dump of an even number of bytes has a character count that is a multiple of four, which makes it valid base64 as well — and the two readings produce completely different bytes. `1B40` is two bytes as hex and three entirely different bytes as base64. So: - A dump written **with its bytes spaced apart** — `1B 40 48 65` — is read as hex. Base64 is not written that way, which is what makes the spacing a reliable signal. - A payload valid as **only one** of the two is read as that one. - A **run-together** payload valid as both is **refused**, in the dialog and in the API alike, with a message asking you to say which it is. Guessing there would import the wrong bytes silently, which is worse than an error. In a script, the reliable thing is simply to state `Format`. In the dialog, the format selector does the same, and the hint under the field tells you what your paste was recognised as and how many bytes that comes to. ## What it is not Import replays bytes into the parser. It does not exercise your application's socket handling, timeouts or reconnects, because nothing is connected — for that, print over TCP as usual. See [Choosing a test printer](/docs/concepts/choosing-a-test-printer). --- # Setup Anything that can print to a network thermal printer can print here. Only the address changes: your printer's host and port instead of a device on the LAN. Pick the page that matches how your software prints. | How your software prints | Page | |---|---| | It asks for a printer host and port | [POS software](/docs/setup/pos-software) | | Through a Windows print queue | [Windows print queue](/docs/setup/windows-driver) | | Through CUPS on Linux or macOS | [CUPS](/docs/setup/linux-cups) | | With an ESC/POS library | [Printing libraries](/docs/setup/libraries) | | It opens a socket itself | [Code samples](/docs/setup/code-samples) | Whichever route you take, the address is the same pair: `virtual-printer.online` and the port shown on your printer's card. See [Raw TCP port 9100](/docs/start/raw-tcp-port-9100) for what that port is. ## Before you start New workspaces have the [IP whitelist](/#settings/whitelist) enabled, holding only the address you created the workspace from. If you are setting up a second machine, add its address first, or its connections will be reset - see [Connection refused or reset](/docs/troubleshooting/connection-refused). ## Networks that block the port Printer ports are non-standard, and plenty of corporate networks allow outbound 80 and 443 only. If a connection works from home and times out at the office, that is the cause: ask for the port to be opened, or self-host inside the network. --- # POS Software Most POS applications print to a network printer by host and port. That is the whole integration: swap the address of the device on your counter for the address of a virtual printer. ## What to enter | Setting | Value | |---|---| | Host / IP address | `virtual-printer.online` (or your own host when self-hosting) | | Port | your printer's port, shown on its card | | Connection type | Network, Ethernet, TCP/IP, raw, or "9100" - the names vary | | Protocol / emulation | ESC/POS for receipts, EPL for labels - match the printer you created | | Encoding / code page | whatever your receipts need; see [Garbled characters](/docs/troubleshooting/garbled-characters) | Avoid any option that mentions a Windows driver, a spooler, USB, or a serial port. Those paths do not open a socket, and the virtual printer only accepts sockets. ## Confirming it works Print anything - a test receipt from the application is ideal - and watch the document appear. If it does not: 1. Check [Workspace Settings -> Access Log](/#settings/connections). A rejected entry means the [IP whitelist](/#settings/whitelist) does not have the terminal's address yet. 2. If nothing is logged, the bytes never arrived: see [Connection refused or reset](/docs/troubleshooting/connection-refused). ## Testing a whole site Terminals usually address several printers - kitchen, bar, receipts. Create one virtual printer per role, give each the width of the hardware it stands in for, and point each terminal at the matching port. Ports are permanent, so the configuration you enter once keeps working. Widths worth knowing: 384 dots for 58 mm paper, 576 dots for 80 mm. A layout that only ever ran on 80 mm paper often breaks on 58 mm, and creating both is the cheapest way to find out before the printer does. --- # Windows Print Queue Use this when the thing you want to test is what a **driver** emits, or when your application prints through the Windows spooler rather than opening a socket itself. If your code builds ESC/POS bytes and writes them to a socket, skip this page - a queue would only get in the way. ## Adding the printer 1. **Settings -> Bluetooth & devices -> Printers & scanners -> Add device**, then *Add manually* when the search finds nothing. 2. Choose **Add a printer using an IP address or hostname**. 3. Device type: **TCP/IP Device**. Hostname: `virtual-printer.online`. Port: your printer's port. Clear *Query the printer and select the driver to use automatically* - there is no SNMP here to answer. 4. When Windows asks for the port type, choose **Custom -> Settings** and confirm the protocol is **Raw** with the port number you entered. This is the step that matters: Raw is what sends the byte stream unaltered. 5. Pick a driver: - **Generic / Text Only** passes text through with almost no additions - the closest thing to printing the bytes yourself. - **Your printer vendor's driver** is the right choice when you want to see exactly what it emits, which is usually the reason to set up a queue at all. ## What you will see Windows drivers are chatty. Expect initialisation sequences, code-page selection and status polls around your content on every job, and expect some of it to be ignored by the emulator - that is normal and documented in [Supported ESC/POS commands](/docs/protocols/escpos/supported-commands). If the queue reports an error, the connection was probably reset by the [IP whitelist](/#settings/whitelist) rather than refused; the [Access Log](/#settings/connections) tells you which. ## Checking the port without a queue ```powershell Test-NetConnection virtual-printer.online -Port 9411 ``` A failure here means the queue was never going to work, and the problem is the address or the network. --- # CUPS on Linux and macOS CUPS speaks to network printers through a `socket://` URI, which is exactly what a virtual printer listens for. ## Adding the queue ```bash lpadmin -p virtual-receipt \ -v socket://virtual-printer.online:9411 \ -E \ -m raw ``` Substitute your own port. `-m raw` is the important part: it creates a queue with no driver, so the bytes you send arrive unchanged. `-E` enables the queue immediately. Print to it the way you print to anything else: ```bash lp -d virtual-receipt receipt.bin ``` For a driver-based queue - when what you want to test is the driver's output rather than your own bytes - replace `-m raw` with the PPD your vendor supplies. ## macOS The same commands work; CUPS is the printing system there too. The graphical *Add Printer* dialog can also do it: hold Option to reveal *Advanced*, choose type **AppSocket/HP JetDirect**, and enter `socket://virtual-printer.online:9411`. ## Checking the queue ```bash lpstat -p virtual-receipt # queue state lpstat -o # jobs waiting ``` A job that sits in the queue rather than completing usually means the connection is being reset - check [Workspace Settings -> Access Log](/#settings/connections), because the [IP whitelist](/#settings/whitelist) is the most likely reason. --- # Printing Libraries Every ESC/POS library has a network transport, and pointing it here is a one-line change: the host and port of your virtual printer instead of the device on your network. The examples below all print the same thing; substitute your own port. ## python-escpos ```python from escpos.printer import Network printer = Network("virtual-printer.online", port=9411) printer.text("Hello from python-escpos\n") printer.cut() printer.close() ``` `Network` is the transport to use - not `Usb`, not `Serial`, and not `File`. ## node-thermal-printer ```javascript const { ThermalPrinter, PrinterTypes } = require('node-thermal-printer'); const printer = new ThermalPrinter({ type: PrinterTypes.EPSON, interface: 'tcp://virtual-printer.online:9411', }); printer.println('Hello from node-thermal-printer'); printer.cut(); await printer.execute(); ``` ## escpos-php ```php use Mike42\Escpos\PrintConnectors\NetworkPrintConnector; use Mike42\Escpos\Printer; $connector = new NetworkPrintConnector("virtual-printer.online", 9411); $printer = new Printer($connector); $printer->text("Hello from escpos-php\n"); $printer->cut(); $printer->close(); ``` ## ESCPOS.NET ```csharp var printer = new ESCPOS_NET.NetworkPrinter( new NetworkPrinterSettings { ConnectionString = "virtual-printer.online:9411" }); var e = new ESCPOS_NET.Emitters.EPSON(); printer.Write(e.PrintLine("Hello from ESCPOS.NET"), e.FullCut()); ``` ## If nothing arrives Libraries differ in when they flush and whether they close the connection. A document is finalized when the socket closes or after a short idle timeout, so a library that keeps the connection open produces nothing until it does - call its close or execute method. See [Nothing appears](/docs/troubleshooting/nothing-appears). ## Writing the bytes yourself No library is required: see [Code samples](/docs/setup/code-samples) for a plain socket in C#, C++, Node.js and Python. --- # Code Samples Printing is a plain TCP socket, so every language does the same three things: connect to `virtual-printer.online` on your printer's port, write the bytes, close the connection. Encoding matters — the samples below use CP437, the default code page; see [Code pages](/docs/protocols) if your receipt has non-ASCII text.
```csharp using System.Net.Sockets; using System.Text; // Raw TCP, like printing to a network printer on 9100 - use your printer's own port. var client = new TcpClient("virtual-printer.online", 9107); var stream = client.GetStream(); var data = Encoding.GetEncoding(437).GetBytes("Hello from Virtual Printer!\n"); stream.Write(data, 0, data.Length); client.Close(); ```
```cpp #include #include #include #pragma comment(lib, "Ws2_32.lib") int main() { WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsaData); SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(9107); // your printer's port from the 9100 block inet_pton(AF_INET, "virtual-printer.online", &addr.sin_addr); connect(sock, (sockaddr*)&addr, sizeof(addr)); const char* data = "Hello from Virtual Printer!\n"; send(sock, data, strlen(data), 0); closesocket(sock); WSACleanup(); return 0; } ```
```javascript const net = require('net'); // Raw TCP, like printing to a network printer on 9100 - use your printer's own port. const client = net.connect({ host: 'virtual-printer.online', port: 9107 }, () => { const buffer = Buffer.from('Hello from Virtual Printer!\n', 'latin1'); client.write(buffer); client.end(); }); ```
```python import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Raw TCP, like printing to a network printer on 9100 - use your printer's own port. sock.connect(('virtual-printer.online', 9107)) data = 'Hello from Virtual Printer!\n'.encode('cp437') sock.sendall(data) sock.close() ```
## Next - [Which port do I get?](/docs/start/raw-tcp-port-9100) - [Driving the printer from autotests](/docs/ci-cd) --- # CI/CD and Automated Tests Virtual Printer is meant to be driven by test code, not only by hand. The printer is a real TCP listener, so the code under test prints exactly the way it prints in production, and everything around that step — creating the printer, reading the result back, injecting faults — happens over the HTTP API. ## The round trip a test makes The HTTP/REST API exists for exactly that, and the whole round trip is scriptable with no hardware and no browser open. A test typically does: 1. `POST /api/workspaces` — create a throwaway workspace (anonymous; returns the workspace token) 2. `POST /api/auth/login` — exchange the token for a bearer access token 3. `POST /api/printers` — create the printer; the response carries `settings.publicHost` and `settings.tcpListenPort` 4. **Print over raw TCP** — the code under test opens a socket to that host and port and writes its bytes, the same call path it uses in production 5. `GET /api/printers/{id}/documents/canvas` — read the parsed document back and assert on the rendered elements: text, alignment, fonts, barcodes, QR codes, images, cuts Instead of polling in step 5, subscribe to the SSE stream `GET /api/printers/{id}/documents/canvas/stream` and wait for the document event. Documents come back as structured JSON rather than a picture, so assertions read the receipt the way a person would - see [Asserting on a receipt](/docs/ci-cd/assertions). Nothing has to be scraped out of the web interface, and nothing has to be downloaded by hand. Also useful in tests: - `PATCH /api/printers/{id}/operational-flags` — inject paper out, cover open, offline, error or paper near end, and start/stop the listener - `PATCH /api/printers/{id}/drawers` — set the emulated cash-drawer state - `POST /api/printers/{id}/documents/import` — replay a captured payload (base64) as if it had been printed, which is handy for reproducing a bug report - `DELETE /api/printers/{id}/documents` — clear documents between test cases The full endpoint reference, including the request and response shapes, is in [API endpoints](/docs/api/endpoints). ## Should CI use its own workspace? That is the simplest setup: a workspace is created anonymously in a single call, so a pipeline can create one per run and delete it at the end (`DELETE /api/workspaces`), which keeps test documents out of the workspace you use interactively. If you prefer a long-lived CI workspace, keep its token as a pipeline secret and reuse the same printer — its port does not change. --- # Pipelines A pipeline needs a printer that exists for the length of one run and leaves nothing behind. Workspaces are created anonymously in a single call, so the whole lifecycle fits in a job. ## GitHub Actions ```yaml name: receipt-tests on: [push] env: VP_HOST: virtual-printer.online VP_API: https://virtual-printer.online/api jobs: receipts: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Create a throwaway workspace and printer id: printer run: | WORKSPACE_ID=$(uuidgen) PRINTER_ID=$(uuidgen) TOKEN=$(curl -sf -X POST "$VP_API/workspaces" \ -H 'Content-Type: application/json' \ -d "{\"Id\":\"$WORKSPACE_ID\",\"WorkspaceName\":\"ci-${{ github.run_id }}\"}" \ | jq -r .token) ACCESS=$(curl -sf -X POST "$VP_API/auth/login" \ -H 'Content-Type: application/json' \ -d "{\"Token\":\"$TOKEN\"}" | jq -r .accessToken) PORT=$(curl -sf -X POST "$VP_API/printers" \ -H "Authorization: Bearer $ACCESS" \ -H 'Content-Type: application/json' \ -d "{\"Printer\":{\"Id\":\"$PRINTER_ID\",\"DisplayName\":\"ci\"}, \"Settings\":{\"Protocol\":\"EscPos\",\"WidthInDots\":576, \"HeightInDots\":null,\"EmulateBufferCapacity\":false, \"BufferDrainRate\":null,\"BufferMaxCapacity\":null}}" \ | jq -r .settings.tcpListenPort) echo "access=$ACCESS" >> "$GITHUB_OUTPUT" echo "printer=$PRINTER_ID" >> "$GITHUB_OUTPUT" echo "port=$PORT" >> "$GITHUB_OUTPUT" - name: Run the tests against it env: PRINTER_HOST: ${{ env.VP_HOST }} PRINTER_PORT: ${{ steps.printer.outputs.port }} run: ./gradlew test # or npm test, dotnet test, pytest ... - name: Assert on what was printed run: | curl -sf "$VP_API/printers/${{ steps.printer.outputs.printer }}/documents/canvas?limit=10" \ -H "Authorization: Bearer ${{ steps.printer.outputs.access }}" > receipts.json jq -e '.result.items | length > 0' receipts.json - name: Delete the workspace if: always() run: | curl -sf -X DELETE "$VP_API/workspaces" \ -H "Authorization: Bearer ${{ steps.printer.outputs.access }}" ``` Two details that matter more than they look: - **The workspace is deleted in an `always()` step.** Without it a failed run leaves a workspace behind, and they accumulate. - **The IP whitelist.** A new workspace allows only the address that created it - which, here, is the runner itself, so a pipeline that creates its own workspace needs no whitelist changes at all. A pipeline reusing a long-lived workspace does: hosted runners have changing addresses, so either add the ranges or turn the whitelist off for that workspace and accept that the port is public. ## GitLab CI The same shape, with `before_script` for setup and `after_script` for the cleanup that must run regardless of outcome: ```yaml receipts: image: alpine:3 before_script: - apk add --no-cache curl jq - ./ci/create-printer.sh # writes PRINTER_PORT and VP_ACCESS to printer.env script: - . ./printer.env && ./run-tests.sh after_script: - . ./printer.env && curl -sf -X DELETE "$VP_API/workspaces" -H "Authorization: Bearer $VP_ACCESS" ``` ## Jenkins In a declarative pipeline, create the printer in a `stage` and delete it in `post { always { ... } }`. The commands are identical; only the syntax around them changes. ## Keeping the setup out of the pipeline Everything above is `curl` so that it reads the same in any CI system, but this logic usually belongs in the test suite itself rather than in YAML - see [Automated tests](/docs/ci-cd), which does the same round trip from test code. --- # Asserting on a Receipt `GET /api/printers/{id}/documents/canvas` returns documents as the elements the parser produced - runs of text with their styling, barcodes, images, cuts - rather than a rendered picture. That is what makes them worth asserting on. ## Assert on meaning, not on pixels Good assertions survive a layout change that a human would call correct: - the total appears once, with the expected value; - the barcode carries the order number, in the expected symbology; - the receipt ends with a cut; - the logo is present; - nothing is marked as an error. Poor assertions break on every change: exact dot coordinates, the full text of the receipt compared byte for byte, the number of line feeds. The exception is a deliberate golden-master test, where comparing the whole document *is* the point. Keep those few and update them consciously. ## Waiting for the document Two ways, and the second is better in a pipeline: - **Poll** `documents/canvas?limit=1` until it appears. Simple, and fine when you already have a retry helper. - **Subscribe** to `GET /api/printers/{id}/documents/canvas/stream`, a Server-Sent Events stream that emits each completed document. No polling interval to tune, and no race between the assertion and the print. Remember that a document is only finalized when the connection closes or after a short idle timeout. A test that asserts while its own socket is still open will always find nothing - see [Nothing appears](/docs/troubleshooting/nothing-appears). ## Isolating test cases `DELETE /api/printers/{id}/documents` clears the history, which keeps one test from asserting on the receipt another test printed. Call it between cases, or create a printer per case - both are cheap. ## Replaying a captured payload `POST /api/printers/{id}/documents/import` takes a base64 payload and processes it as if it had been printed. When a bug report arrives with a byte dump attached, this turns it into a document you can look at, without reproducing whatever produced it. It is also a way to write a regression test for a stream you cannot easily generate: keep the bytes as a fixture, import them, assert on the result. --- # Fault Injection The interesting bugs in printing code are not in the happy path. They are in what happens when the paper runs out halfway through a receipt and the application reports success anyway. Real hardware makes this awkward to stage: you have to physically open the cover, pull the roll, or unplug the network at the right moment. Here it is an HTTP call, which means it can run on every commit. ## Hardware faults ```bash curl -sf -X PATCH "$VP_API/printers/$PRINTER_ID/operational-flags" \ -H "Authorization: Bearer $ACCESS" \ -H 'Content-Type: application/json' \ -d '{"IsPaperOut": true}' ``` The flags available are `IsPaperOut`, `IsPaperNearEnd`, `IsCoverOpen`, `IsOffline` and `HasError`. Set one, print, and assert on how your application reacts. Clear it by sending `false`. What makes this more than a flag on a screen: **the printer answers status requests accordingly**. A client that polls `DLE EOT n`, or reads Automatic Status Back, receives the status bytes a real printer in that condition would send. If your application checks the printer before printing, this is what exercises that code. ## Taking the listener down The same endpoint sets `TargetState` to `Stopped` or `Started`. A stopped printer refuses connections outright, which is how you test the reconnect path - a different failure from a printer that is reachable but not ready. ## A slow printer Real thermal printers have a small receive buffer and print slowly. A long receipt sent faster than the printer prints it fills that buffer, and what happens next is exactly the case worth testing. Enable buffer emulation on the printer, giving it a capacity and a drain rate. The buffer then fills as bytes arrive and empties at the rate you set, and: - a job that outruns the capacity is marked in the document with a **buffer overflow**, so a test can assert that the receipt did not survive intact; - while the buffer holds data the printer reports itself **busy**, and reports **full** when it is at capacity, which is what a client polling the status sees; - the buffered byte count is visible in the runtime status and on its live stream, so a test can watch it fill and drain. ## The cash drawer ```bash curl -sf -X PATCH "$VP_API/printers/$PRINTER_ID/drawers" \ -H "Authorization: Bearer $ACCESS" \ -H 'Content-Type: application/json' \ -d '{"Drawer1State": "Closed"}' ``` Drawer state is readable from the runtime status, and an ESC/POS pulse from your application opens the emulated drawer - enough to assert that the till opens when it should, and stays shut when it should not. ## A worked order A test for "the paper runs out mid-receipt" usually looks like this: 1. Create the printer, print a normal receipt, assert it arrived. 2. Set `IsPaperOut`. 3. Print the next receipt. 4. Assert your application surfaced an alert, retried, or queued the order - whatever it is supposed to do - rather than reporting success. 5. Clear the flag and assert it recovers. Step 4 is the one that finds real bugs. --- # Printer Protocols A virtual printer is created for one protocol, because the byte stream is only meaningful once you know which language it is written in. Pick it when you create the printer; you cannot feed EPL to an ESC/POS printer and expect a label. ## ESC/POS The receipt language. Originally Epson's, now the de facto standard for thermal receipt printers from dozens of vendors. It is a stream of escape sequences interleaved with the text to print: set bold, print a line, feed, print a barcode, cut. This is what a POS application emits when it prints a customer receipt, and it is the protocol most users of this service are testing. Supported here including text styling, code pages, barcodes and QR codes, raster images, NV logos, cash-drawer pulses, paper cuts, and real-time status replies. ## EPL Eltron Programming Language, used by label printers. Unlike ESC/POS it is line-oriented and page-at-a-time: you describe the fields of a label, then print N copies. Supported here for label layout, text, and barcodes. ## TSPL TSC's label language, similar in spirit to EPL. In development — not yet available when creating a printer. ## ZPL Zebra's label language, the other half of the label world alongside EPL. In development — not yet available when creating a printer. ## Which one am I sending? If you do not know what your application emits, print it here anyway and look at the parsed result: a document full of unrecognized commands usually means the printer's protocol does not match the stream. --- # Supported ESC/POS Commands This is not a copy of the ESC/POS specification. For what a command means, Epson's own manuals are the authority. What this page answers is the question they cannot: **what happens to that command here.** Every row is generated from the command descriptors the parser registers, so the table cannot claim support that does not exist, and a command added to the parser appears here on the next deploy. ## How to read it - **Renders** - the command affects the document you see: text styling, position, barcodes, images, cuts, drawer pulses. - **Answers on the socket** - the printer writes bytes back to your client, and what it answers reflects the fault flags you have set on the printer. - **Ignored** - the command is recognised and consumed so the byte stream stays in sync, but the emulator has nothing to do with it. The reason is in the same cell. An ignored command is not an error and does not mark the document. A command **absent from this table** is a different matter: the parser does not know it, the bytes are reported as unrecognised, and the document is marked with an error. If you hit one that a real printer accepts, that is worth an email to [support@virtual-printer.online](mailto:support@virtual-printer.online) - see [Document marked with an error](/docs/troubleshooting/document-has-errors). ## The matrix | Command | What it does | Bytes | In the emulator | | --- | --- | --- | --- | | BEL | buzzer/beeper | 07 | Supported | | CAN | cancel print data in page mode | 18 | Ignored in standard mode (page mode is not supported) | | Carriage Return (CR) | legacy compatibility command, ignored by printer | 0D | Supported | | DC1 | device select (legacy software flow control) | 11 | Ignored: the virtual printer is always selected | | DC2 | cancel condensed printing | 12 | Supported. Rendered as Font A. | | DC3 | device deselect (legacy software flow control) | 13 | Ignored: the virtual printer is always selected | | DC4 | cancel the double-width mode selected by SO | 14 | Supported | | DLE DC4 fn ... | real-time requests (drawer pulse, power-off, buzzer, buffer clear) | 10 14 fn | Supported. The drawer pulse drives the emulated cash drawer; the remaining real-time hardware actions are not modeled and are ignored. | | DLE ENQ n | real-time recovery from a recoverable error | 10 05 n | Ignored: the virtual printer has no recoverable error state to clear | | DLE EOT n | real-time printer status | 10 04 n | Supported | | ESC | n - enable/disable underline mode | 1B 2D n (00=off, 01=on) | Supported | | ESC ! | select print mode | 1B 21 n | Supported | | ESC $ nL nH | set absolute print position | 1B 24 nL nH (position = nL + nH * 256 dots from the beginning of the line) | Supported | | ESC % n | select or cancel the user-defined character set | 1B 25 n | Ignored: user-defined glyphs are not rendered | | ESC & y c1 c2 [x1 d1...dk] ... | define user-defined characters | 1B 26 y c1 c2 | Supported. (per character: one width byte x followed by y * x data bytes). The payload is parsed only to keep the byte stream in sync; the glyphs are not rendered. | | ESC * m nL nH d1...dk | select bit image mode | 1B 2A m nL nH | Supported. (m: 0/1 = 8-dot single/double density, 32/33 = 24-dot). | | ESC 0 | select 1/8-inch line spacing (legacy preset) | 1B 30 | Supported | | ESC 1 | select 7/72-inch line spacing (legacy preset) | 1B 31 | Supported | | ESC 2 | set default line spacing (approx. 30 dots) | 1B 32 | Supported | | ESC 3 n | set line spacing | 1B 33 0xNN | Supported | | ESC 4 | enable italic mode | 1B 34 | Supported | | ESC 5 | disable italic mode | 1B 35 | Supported | | ESC = n | select peripheral device | 1B 3D n | Ignored: there is only one (virtual) device on the line | | ESC @ | reset printer | 1B 40 | Supported | | ESC a | select justification | 1B 61 n (00=left, 01=center, 02=right) | Supported | | ESC c 0/1/3/4/5 n | paper type, paper sensor and panel button configuration | 1B 63 30/31/33/34/35 n | Ignored: the virtual printer has one paper station, and | | ESC d n | print and feed n lines | 1B 64 n | Supported | | ESC D n1 ... nk NUL | set horizontal tab positions | 1B 44 n1 | Supported. nk 00. | | ESC E n | enable/disable emphasized (bold) mode | 1B 45 n (00=off, 01=on) | Supported | | ESC F | cancel emphasized (bold) mode | 1B 46 | Supported | | ESC g | select 15 cpi pitch | 1B 67 | Supported. Rendered as Font B. | | ESC G n | enable/disable double-strike mode | 1B 47 n (00=off, 01=on) | Supported | | ESC i | Partial cut (one point left uncut) | 1B 69 | Supported | | ESC J n | print and feed paper by n dots | 1B 4A n | Supported | | ESC m | Partial cut (three points left uncut) | 1B 6D | Supported | | ESC M n | select character font | 1B 4D n (00=Font A, 01=Font B) | Supported | | ESC P | select 10 cpi pica pitch (the dot-matrix default) | 1B 50 | Supported. Rendered as Font A. | | ESC p m t1 t2 | cash drawer pulse | 1B 70 0xMM 0xT1 0xT2 | Supported. m selects the drawer kick-out connector pin - 0 and 48 mean pin 2, 1 and 49 mean pin 5 - and is normalized to the 0/1 drawer index the emulated drawers are addressed by. The ON interval is t1 x 2 ms and the OFF interval is t2 x 2 ms; a printer that is given t2 < t1 stretches the OFF interval to match the ON interval. | | ESC r n | select print color on two-color paper | 1B 72 n (0/48 black, 1/49 red) | Supported | | ESC R n | select international character set | 1B 52 n (0=USA, 1=France, 2=Germany, ...) | Supported | | ESC SP n | set right-side character spacing | 1B 20 n | Supported | | ESC t | select character code table | 1B 74 n | Supported | | ESC u n | transmit peripheral device (drawer) status | 1B 75 n | Supported. Legacy counterpart of GS r 2. | | ESC v | transmit paper sensor status | 1B 76 | Supported. Legacy counterpart of GS r 1. | | ESC V n | turn 90-degree clockwise character rotation on/off | 1B 56 n (0/48 off; 1/49 and 2/50 on) | Supported. Distinct from GS V (1D 56), which cuts paper. | | ESC \ nL nH | set relative print position | 1B 5C nL nH (offset = signed 16-bit nL + nH * 256 dots from the current position) | Supported | | ESC { n | enable/disable upside-down mode | 1B 7B n | Supported | | FS & | select Chinese (GB2312) character set | 1C 26 | Supported | | FS . | cancel Kanji character mode | 1C 2E | Ignored: single-byte code pages are always active | | FS p n m | print NV logo n with scale mode m | 1C 70 n m | Supported | | FS q n [xL xH yL yH d1...dk] x n | define NV bit images | 1C 71 n | Supported. (per image: width = (xL + xH*256) * 8 dots, height = (yL + yH*256) * 8 dots, k = x * y * 8 bytes of column-format data). Replaces all previously stored NV logos. | | GS ! n | select character size | 1D 21 n | Supported | | GS ( A pL pH fn m | execute a test print | 1D 28 41 pL pH | Ignored: the virtual printer has no self-test page | | GS ( J pL pH fn m | absent from the public ESC/POS reference | 1D 28 4A pL pH | Ignored: only its length is known, and it has no visible effect on | | GS ( k | 2D symbology configuration and workflow (model, module size, EC level, | 1D 28 6B pL pH cn fn [data] (cn: 0x30 PDF417, 0x31 QR, 0x35 Aztec, 0x36 DataMatrix) | Supported | | GS ( K pL pH fn m | select print control method(s) | 1D 28 4B pL pH | Supported. Function 49 selects the print density and is honoured, because it decides how dark the paper actually comes out. The remaining functions - print control mode (48), print speed (50) and the number of thermal head energizing parts (97) - describe how the head is driven and leave no trace in a rendered document, so they stay ignored. | | GS ( L pL pH ... | graphics data store/print workflow | 1D 28 4C pL pH | Supported | | GS ( M pL pH fn m | customize printer control values | 1D 28 4D pL pH | Ignored: the virtual printer keeps no persistent setting areas | | GS ( N pL pH fn m | two-color print configuration | 1D 28 4E pL pH fn m (fn=48 selects the character color: m=48 black, m=49 red) | Supported | | GS ( P pL pH fn ... | page mode control | 1D 28 50 pL pH | Ignored: page mode is not supported | | GS ( Q pL pH fn ... | draw a line (fn = 48) or a rectangle (fn = 49) into the | 1D 28 51 pL pH | Ignored: the command is only valid while page mode is selected | | GS ( V pL pH fn m [n] | paper cut functions | 1D 28 56 pL pH | Supported. Function 48 cuts, function 49 feeds n motion units and then cuts; both map onto the same cut element GS V produces. Other functions (cut reservation) only arrange for a cut to happen later, so they are consumed but not turned into a cut here. | | GS * x y d1...dk | define download bit image | 1D 2A x y | Supported. (width = x * 8 dots, height = y * 8 dots, k = x * y * 8 bytes). | | GS / m | print download bit image | 1D 2F m (0/48 normal, 1/49 double width, 2/50 double height, 3/51 quadruple) | Supported | | GS 8 L p1 p2 p3 p4 ... | long graphics data store workflow | 1D 38 4C p1 p2 p3 p4 | Supported | | GS a n | enable/disable Automatic Status Back | 1D 61 n (bit mask of the enabled status types; 0 disables ASB) | Supported | | GS B n | enable/disable reverse (white-on-black) mode | 1D 42 n (00=off, 01=on) | Supported | | GS b n | turn smoothing mode on/off | 1D 62 n | Ignored: the virtual printer does not model glyph smoothing | | GS h n | set the height of the next barcode in dots | 1D 68 n | Supported | | GS H n | choose where the human-readable digits are printed, relative to the barcode | 1D 48 n | Supported | | GS I n | transmit printer ID | 1D 49 n (1-3 single byte model/type/version, 65-69 information strings) | Supported | | GS k | print a barcode of type m from the data that follows | 1D 6B m d1...dk 00, or 1D 6B m n d1...dn | Supported | | GS L nL nH | set left margin | 1D 4C nL nH (margin = nL + nH * 256 dots) | Supported | | GS r n | transmit status | 1D 72 n (1/49 paper sensor, 2/50 drawer kick-out connector) | Supported | | GS v 0 m xL xH yL yH [data] | raster bit image print | 1D 76 30 m xL xH yL yH | Supported | | GS V m [n] | paper cut with mode | 1D 56 0xMM [0xNN] | Supported | | GS w n | set the module width (the width of the narrowest bar) of the next barcode | 1D 77 n | Supported | | GS W nL nH | set print area width | 1D 57 nL nH (width = nL + nH * 256 dots) | Supported | | HT | move to next horizontal tab stop | 09 | Supported | | Line Feed | print buffer and feed one line | 0A | Supported | | SI | select condensed printing | 0F | Supported. Rendered as Font B. | | SO | select double-width printing for one line | 0E | Supported. Canceled by LF or DC4. | ## Related - [Printer protocols](/docs/protocols) - ESC/POS, EPL, TSPL and where ZPL stands - [Garbled characters](/docs/troubleshooting/garbled-characters) - code pages, the usual cause of wrong glyphs - [How it works](/docs/concepts/how-it-works) - what the parser produces from these commands --- # HTTP API Virtual Printer speaks two protocols, and it matters which one does what. - **HTTP/REST** — create workspaces and printers, read parsed documents back, inject hardware faults, subscribe to event streams. Everything under `/api`. - **Raw TCP** — the printing itself. A client opens a socket to `settings.publicHost` on `settings.tcpListenPort` and writes ESC/POS or EPL bytes. This step is deliberately *not* HTTP and is therefore not in the OpenAPI spec. A test or integration normally uses both: HTTP to set the printer up and to assert on the result, raw TCP in between for the part that must behave like real hardware. ## Reference - [Endpoint reference](/docs/api/endpoints) — every endpoint with its request and response shapes, the `problem+json` error map, the SSE streams, and the full end-to-end scenario. - [Printer status API](/docs/api/printer-status) — start/stop, operational flags, cash drawer, and status streaming in detail. - [CI/CD and automated tests](/docs/ci-cd) — the same endpoints arranged as a test workflow. ## OpenAPI A live spec is generated from the controllers and served by the application itself: - Swagger UI: [`/swagger`](/swagger) - Raw spec: [`/swagger/v1/swagger.json`](/swagger/v1/swagger.json) It covers the HTTP surface only, for the reason above. ## Conventions worth knowing up front - JSON is **PascalCase** (`Token`, `AccessToken`, `Id`). - Null properties are omitted from responses, with one deliberate exception: `Printer.LastDocumentReceivedAt` is always present and may be `null`. - IDs for workspaces and printers are **supplied by the caller** as GUIDs, which makes creates easy to assert against and re-run. - Every authorized endpoint takes `Authorization: Bearer ` from [`POST /api/auth/login`](/docs/api/endpoints). --- # virtual-printer.online API — endpoint reference The HTTP/REST contract for the virtual-printer.online API: endpoints, request/response shapes, the error format, and the end-to-end print-and-verify flow. > This page is the served copy. The canonical source is **`doc/api-endpoints.md`** in > the repository (used by integration/autotests) — keep the two in sync. > **Scope — two protocols.** Managing workspaces, printers and reading documents is > **HTTP/REST** (this document + Swagger). **Printing itself is raw TCP**: a client > opens a socket to the printer's listener (`Settings.TcpListenPort` / > `Settings.PublicHost`) and writes ESC/POS or EPL bytes. The printed document then > becomes readable over HTTP. The TCP step is **not** in OpenAPI — see > [End-to-end scenario](#end-to-end-scenario). There is also a live OpenAPI spec generated from the controllers (covers the HTTP surface only): **`/swagger`** (UI) and **`/swagger/v1/swagger.json`** (raw), enabled in every environment, Production included. --- ## Conventions - **Base path:** all endpoints are under `/api` (except the SSE notes below, which are also under `/api`). - **JSON casing:** responses are **camelCase** — `token`, `accessToken`, `id`. No naming policy is configured, so the ASP.NET Core default applies. Request bodies are matched case-insensitively, so `Token` and `token` are both accepted on the way in - it is only what comes back that is camelCase. - **Null omission:** properties that are `null` are omitted from responses (`DefaultIgnoreCondition = WhenWritingNull`). The exceptions are the timestamps `printer.lastDocumentReceivedAt`, `printer.lastApiReadAt`, `printer.lastWebReadAt`, `workspace.lastApiReadAt` and `workspace.lastWebReadAt`, which are always emitted (may be `null`). - **Auth header:** `Authorization: Bearer ` on every `[Authorize]` endpoint. Get the token from [`POST /api/auth/login`](#post-apiauthlogin). - **Last-read stamps:** reading a printer's documents (`GET …/documents/canvas`, and each document delivered by `GET …/documents/canvas/stream`) records the UTC time of the read on the printer and on its workspace: `lastApiReadAt` for API clients, `lastWebReadAt` for the bundled web UI, which marks its calls with the `X-Client: web` header. Any request without that header is an API read. An admin workspace reading another workspace's printer records nothing. Reading printer metadata (`GET /api/printers`, `GET /api/printers/{id}`) is not a read. - **IDs are client-supplied GUIDs.** Create requests for workspaces and printers carry the `Id` — the caller generates it. This makes creates idempotent-ish and easy to assert against in tests. ### Error format All unhandled exceptions are converted by `ExceptionHandlingMiddleware` to `application/problem+json`: ```json { "Status": 404, "Detail": "Printer not found.", "Instance": "/api/printers/3f2a…" } ``` | Exception | HTTP status | | ---------------------------------- | ----------- | | `AuthenticationFailedException` | **401** | | `ForbiddenException` | **403** | | `PrinterNotFoundException` | **404** | | `BadRequestException`, `ArgumentException`, `ValidationException` | **400** | | `PrinterListenerStartFailedException` | **500** | | any other | **500** | `OperationCanceledException` and `StreamDisconnectedException` produce **no** response body (expected client/SSE disconnects). --- ## End-to-end scenario The canonical flow every printing test follows: **register → login → create printer → print over TCP → verify the parsed document**. (This is the flow encoded in `ProtocolTestsBase` / `PrintersControllerTests.Documents`.) ``` 1. POST /api/workspaces ── HTTP ─▶ { Id, Name, Token } 2. POST /api/auth/login (Token) ── HTTP ─▶ { AccessToken } → Authorization: Bearer 3. POST /api/printers ── HTTP ─▶ { …, Settings: { TcpListenPort, PublicHost } } 4. connect PublicHost:TcpListenPort── TCP ─▶ write ESC/POS bytes, then close (or idle-timeout) 5. GET …/{id}/documents/canvas ── HTTP ─▶ CanvasDocumentListResponseDto (assert here) ``` ### 1. Register a workspace (anonymous) ```bash curl -sX POST http://localhost:8080/api/workspaces \ -H 'Content-Type: application/json' \ -d '{ "Id": "11111111-1111-1111-1111-111111111111", "WorkspaceName": "autotest" }' ``` ```json { "id": "11111111-1111-1111-1111-111111111111", "name": "autotest", "token": "brave-tiger-1042-a1b2c3d4e5f60718" } ``` The `Token` is shown **once**, at creation. Persist it for step 2. ### 2. Login → access token (anonymous) ```bash curl -sX POST http://localhost:8080/api/auth/login \ -H 'Content-Type: application/json' \ -d '{ "Token": "brave-tiger-1042-a1b2c3d4e5f60718" }' ``` ```json { "accessToken": "eyJhbGciOi…", "tokenType": "Bearer", "expiresInSeconds": 86400, "workspace": { "id": "1111…", "name": "autotest", "role": "User", "documentRetentionDays": 90, "tcpWhitelistEnabled": false, "tcpWhitelistEntries": "", "createdAt": "2026-06-15T10:00:00+00:00" } } ``` Use `AccessToken` as `Authorization: Bearer …` for all subsequent calls. ### 3. Create a printer ```bash curl -sX POST http://localhost:8080/api/printers \ -H 'Authorization: Bearer eyJhbGciOi…' -H 'Content-Type: application/json' \ -d '{ "Printer": { "Id": "22222222-2222-2222-2222-222222222222", "DisplayName": "T-88" }, "Settings": { "Protocol": "EscPos", "WidthInDots": 512, "HeightInDots": null, "EmulateBufferCapacity": false, "BufferDrainRate": null, "BufferMaxCapacity": null } }' ``` The request carries **no port**: `PrinterSettingsDto` has no `TcpListenPort` field, and a port sent anyway is ignored. The server assigns one — you cannot ask for 9100. Response (`PrinterResponseDto`) — note **`Settings.TcpListenPort`** and **`Settings.PublicHost`**: that is where you print. ```json { "printer": { "id": "2222…", "displayName": "T-88", "ownerWorkspaceId": "1111…", "ownerWorkspaceName": "autotest", "isPinned": false, "lastViewedDocumentId": null, "lastDocumentReceivedAt": null, "lastApiReadAt": null, "lastWebReadAt": null }, "settings": { "protocol": "EscPos", "widthInDots": 512, "heightInDots": null, "tcpListenPort": 9101, "emulateBufferCapacity": false, "bufferDrainRate": null, "bufferMaxCapacity": null, "publicHost": "localhost" }, "operationalFlags": { "printerId": "2222…", "targetState": "Started", … }, "runtimeStatus": { "printerId": "2222…", "state": "Started", … } } ``` **Port assignment.** Ports come from the standard printer block that starts at **9100** — the port a physical network thermal printer listens on. 9100 itself is the anchor of the range and is never handed out; the server takes the highest port already assigned and returns the next one up. On a fresh instance that is 9101, as above. On the public virtual-printer.online deployment the low ports are held by printers registered earlier, so a create today comes back with the next free port above them. The port is permanent for the life of the printer and is never recycled, so a client can persist `PublicHost:TcpListenPort` and reuse it. ### 4. Print — raw TCP (NOT HTTP) Open a socket to `PublicHost:TcpListenPort` and write the print bytes. A document is finalized when the connection closes **or** after the listener idle-timeout. ```bash printf 'Hello, world\n\n\n' | nc localhost 9101 # local instance PORT=... # Settings.TcpListenPort from step 3 printf 'Hello, world\n\n\n' | nc virtual-printer.online "$PORT" ``` In C# tests this is short-circuited via `TestPrinterListenerFactory` / `TestPrinterChannel.SendToServerAsync(bytes)` instead of a real socket — a **real external integration test must open the actual TCP socket** on `TcpListenPort`. ### 5. Verify the parsed document (HTTP) ```bash curl -s 'http://localhost:8080/api/printers/2222…/documents/canvas?limit=10' \ -H 'Authorization: Bearer eyJhbGciOi…' ``` Returns `CanvasDocumentListResponseDto` — assert against `Result.Items[].Canvases[].Items` (the rendered elements). To wait for the document instead of polling, subscribe to the SSE stream `GET …/{id}/documents/canvas/stream` (see [SSE](#sse-streaming-endpoints)). --- ## Endpoint reference `A` = requires `Authorization: Bearer`. `—` = anonymous. ### Auth — `/api/auth` #### POST /api/auth/login `—` · Exchange a workspace token for a JWT. → **200** `LoginResponseDto`; **401** if the token is unknown. ``` Request : LoginRequestDto(string Token) Response: LoginResponseDto(string AccessToken, string TokenType, long ExpiresInSeconds, WorkspaceDto Workspace) ``` #### POST /api/auth/logout `A` · No-op placeholder (JWT is stateless). → **200**. ### Workspaces — `/api/workspaces` #### POST /api/workspaces `—` · Create a workspace. → **200** `WorkspaceResponseDto` (**includes the one-time `Token`**). ``` Request : CreateWorkspaceRequestDto(Guid Id, string WorkspaceName) Response: WorkspaceResponseDto(Guid Id, string Name, string Token) ``` #### GET /api/workspaces `A` · Current workspace. → **200** `WorkspaceDto` (no `Token`). ``` WorkspaceDto(Guid Id, string Name, DateTimeOffset CreatedAt, string Role, int DocumentRetentionDays, bool TcpWhitelistEnabled, string TcpWhitelistEntries, DateTimeOffset? LastApiReadAt, DateTimeOffset? LastWebReadAt) ``` `LastApiReadAt` / `LastWebReadAt` are the UTC times documents of this workspace were last read through the API and from the web UI (see *Last-read stamps* in Conventions); `null` until the first read. #### PATCH /api/workspaces `A` · Partial update (all fields optional/nullable). → **200** `WorkspaceDto`; **400** on validation. ``` Request: UpdateWorkspaceRequestDto(string? Name, int? DocumentRetentionDays, bool? TcpWhitelistEnabled, string? TcpWhitelistEntries) ``` #### DELETE /api/workspaces `A` · Delete the current workspace and its data. → **204**. #### GET /api/workspaces/summary `A` · → **200** `WorkspaceSummaryDto(int TotalPrinters, long TotalDocuments, long DocumentsLast24h, DateTimeOffset? LastDocumentAt, DateTimeOffset CreatedAt)`. #### GET /api/workspaces/admin-statistics `A` (admin) · → **200** `AdminWorkspaceStatisticsDto`; **403** for non-admin workspaces. Contains aggregate counts plus `Workspaces: AdminWorkspaceStatisticsRowDto[]` (per-workspace rows, each carrying the workspace's `LastApiReadAt` / `LastWebReadAt`). See `AdminWorkspaceStatisticsDto.cs` for the full field list. #### GET /api/workspaces/greeting `—` · Localized greeting strings, cached 300 s. → **200** `GreetingResponseDto(string? Morning, string? Afternoon, string? Evening, string General)`. #### GET /api/workspaces/retention/cleanup-summary `A` · Preview of what a retention cleanup would delete. → **200** `DocumentRetentionCleanupSummaryDto(int ExpiredDocuments, int RetentionMediaFiles)`. #### POST /api/workspaces/retention/cleanup `A` · Run a cleanup. → **200** `DocumentRetentionCleanupResultDto(int DeletedDocuments, int DeletedMedia)`. ``` Request: RunDocumentRetentionCleanupRequestDto(int MaxDocuments, int? RetentionDaysOverride) ``` > Admin note: a `RetentionDaysOverride` of `0` deletes **everything**, across all > workspaces. Use with care in shared test environments. #### GET /api/workspaces/connections `A` · Recent TCP connection attempts (for the whitelist UI). → **200** `TcpConnectionEntryDto(string ClientIp, DateTimeOffset ConnectedAt, bool Allowed, string ConnectionType)[]`. ### Printers — `/api/printers` #### POST /api/printers `A` · Create a printer (and start its TCP listener). → **200** `PrinterResponseDto`. ``` Request : CreatePrinterRequestDto(PrinterDto Printer, PrinterSettingsDto Settings) PrinterDto(Guid Id, string DisplayName) PrinterSettingsDto(string Protocol, int WidthInDots, int? HeightInDots, bool EmulateBufferCapacity, decimal? BufferDrainRate, int? BufferMaxCapacity) Response: PrinterResponseDto(PrinterDto Printer, PrinterSettingsDto Settings, PrinterOperationalFlagsDto? OperationalFlags, PrinterRuntimeStatusDto? RuntimeStatus) ``` Response DTOs: ``` PrinterDto (Guid Id, string DisplayName, Guid OwnerWorkspaceId, string? OwnerWorkspaceName, bool IsPinned, Guid? LastViewedDocumentId, DateTimeOffset? LastDocumentReceivedAt, DateTimeOffset? LastApiReadAt, DateTimeOffset? LastWebReadAt) PrinterSettingsDto (string Protocol, int WidthInDots, int? HeightInDots, int TcpListenPort, bool EmulateBufferCapacity, decimal? BufferDrainRate, int? BufferMaxCapacity, string PublicHost) PrinterOperationalFlagsDto(Guid PrinterId, string TargetState, DateTimeOffset UpdatedAt, bool IsCoverOpen, bool IsPaperOut, bool IsOffline, bool HasError, bool IsPaperNearEnd) PrinterRuntimeStatusDto(Guid PrinterId, string State, DateTimeOffset UpdatedAt, int? BufferedBytes, int? BufferedBytesDeltaBps, string? Drawer1State, string? Drawer2State) ``` `Protocol` is `EscPos` or `Epl`. `State`/`TargetState` are `Started` / `Stopped`. `LastApiReadAt` / `LastWebReadAt` are the UTC times this printer's documents were last read through the API and from the web UI (see *Last-read stamps* in Conventions); `null` until the first read. #### GET /api/printers `A` · All printers for the workspace. → **200** `PrinterResponseDto[]`. #### GET /api/printers/{id} `A` · One printer. → **200** `PrinterResponseDto`; **404** if not in the workspace. #### PUT /api/printers/{id} `A` · Replace printer + settings (same body shape as create). → **200** `PrinterResponseDto`. ``` Request: UpdatePrinterRequestDto(PrinterDto Printer, PrinterSettingsDto Settings) ``` #### DELETE /api/printers/{id} `A` · Soft-delete. → **204**. #### POST /api/printers/{id}/pin `A` · Pin/unpin. → **200** `PrinterResponseDto`. ``` Request: PinPrinterRequestDto(bool IsPinned) ``` #### PATCH /api/printers/{id}/operational-flags `A` · Set emulated hardware flags and/or `TargetState` (Started/Stopped). → **200** `PrinterOperationalFlagsDto`. ``` Request: UpdatePrinterOperationalFlagsRequestDto(bool? IsCoverOpen, bool? IsPaperOut, bool? IsOffline, bool? HasError, bool? IsPaperNearEnd, string? TargetState = null) ``` #### PATCH /api/printers/{id}/drawers `A` · Set emulated cash-drawer state. → **200** `PrinterRuntimeStatusDto`. ``` Request: UpdatePrinterDrawerStateRequestDto(string? Drawer1State, string? Drawer2State) ``` #### GET /api/printers/{id}/documents/canvas `A` · Paged rendered documents (newest first). → **200** `CanvasDocumentListResponseDto`. Query: `GetDocumentsRequestDto(int Limit = 20, Guid? BeforeId = null)`. Counts as a read: stamps `lastApiReadAt` (or `lastWebReadAt` with `X-Client: web`) on the printer and its workspace, unless an admin is reading another workspace's printer. ``` CanvasDocumentListResponseDto(PagedResult Result) PagedResult(IReadOnlyList Items, bool HasMore, Guid? NextBeforeId, DateTimeOffset? NextBeforeCreatedAt) RenderedDocumentDto(Guid Id, Guid PrintJobId, Guid PrinterId, DateTimeOffset Timestamp, string Protocol, CanvasDto[] Canvases, string? ClientAddress, int BytesReceived, int BytesSent, string[]? ErrorMessages) CanvasDto(int WidthInDots, int? HeightInDots, IReadOnlyList Items) ``` `CanvasElementDto` is a polymorphic hierarchy — `CanvasTextElementDto`, `CanvasImageElementDto`, `CanvasLineElementDto`, `CanvasBoxElementDto`, `CanvasDebugElementDto`. See `Canvas/Elements/CanvasElementDto.cs` for each shape. #### DELETE /api/printers/{id}/documents `A` · Clear all documents for the printer. → **204**. #### POST /api/printers/{id}/documents/import `A` · Import a raw print payload as if it had been printed. → **204**; **400** with the reason when the payload cannot be decoded. ``` Request: ImportDocumentRequestDto(string Data, string? Format = null) ``` `Data` is the bytes as base64 or as a hex dump; whitespace is ignored in both. `Format` is `"Hex"` or `"Base64"` and is optional — omitted, the format is detected: bytes spaced apart are read as hex, a payload valid as only one form is read as that one, and a run-together payload valid as **both** is refused, because every hex digit is also a base64 character and the two readings differ. See [Import a captured dump](/docs/start/import-a-dump). #### POST /api/printers/{id}/documents/last-viewed `A` · **Stub — returns 501 Not Implemented.** `SetLastViewedDocumentRequestDto(Guid DocumentId)`. ### Media — `/api/media` #### GET /api/media/{mediaId} `—` · Download media (image raster referenced by a canvas element). → **200** binary with `ETag: "sha256:"`; **404** if unknown. --- ## SSE (streaming) endpoints `text/event-stream`; each event `data:` is the JSON of the noted DTO. These are not request/response and are poorly represented in OpenAPI — documented here instead. Cancel by closing the connection (server treats it as a normal disconnect). | Endpoint | Auth | Emits | | --- | --- | --- | | `GET /api/printers/sidebar/stream` | `A` | `PrinterSidebarSnapshotDto` updates | | `GET /api/printers/{id}/runtime/stream` | `A` | `PrinterRuntimeStatusDto` updates | | `GET /api/printers/{id}/documents/canvas/stream` | `A` | `RenderedDocumentDto` on each completed print (**404** if the printer isn't visible); every delivered document counts as a read (see *Last-read stamps*) | --- ## Test-harness notes - **JWT secret guard:** the app exits at startup if `Jwt:SecretKey` is missing, shorter than 32 chars, or still the `your-secret-key…` placeholder. Integration hosts must supply a valid secret (≥32 chars). - **In-memory DB:** `dotnet test` swaps SQLite for a named in-memory database; a keeper connection in `ApiFactory` keeps it alive for the test's lifetime. - **Static files / docs** are skipped when the environment is `Test`. - **Document completion** happens on TCP socket close or after the listener idle-timeout (`PrinterConstants.ListenerIdleTimeoutMs`) — give the document a moment, or use the SSE stream, before asserting on `…/documents/canvas`. --- # Printer Status API This document describes printer status workflows: start/stop, operational flags, drawer control, on-demand reads, and streaming. Notes: - JSON responses omit null fields globally. - Drawer state "OpenedByCommand" is emitted by ESC/POS pulses and cannot be set via API. ## Types (TypeScript) type PrinterTargetState = "Started" | "Stopped"; type PrinterListenerState = "Starting" | "Started" | "Stopped" | "Error"; type DrawerState = "Closed" | "OpenedManually" | "OpenedByCommand"; type PrinterDto = { id: string; displayName: string; isPinned: boolean; lastViewedDocumentId: string | null; lastDocumentReceivedAt: string | null; }; type PrinterSettingsDto = { protocol: "EscPos"; widthInDots: number; heightInDots: number | null; tcpListenPort: number; emulateBufferCapacity: boolean; bufferDrainRate: number | null; bufferMaxCapacity: number | null; }; type PrinterOperationalFlagsDto = { printerId: string; targetState: PrinterTargetState; updatedAt: string; isCoverOpen: boolean; isPaperOut: boolean; isOffline: boolean; hasError: boolean; isPaperNearEnd: boolean; }; type PrinterRuntimeStatusDto = { printerId: string; state: PrinterListenerState; updatedAt: string; bufferedBytes: number | null; drawer1State: DrawerState | null; drawer2State: DrawerState | null; }; type PrinterResponseDto = { printer: PrinterDto; settings: PrinterSettingsDto; operationalFlags: PrinterOperationalFlagsDto | null; runtimeStatus: PrinterRuntimeStatusDto | null; }; type PrinterSidebarSnapshotDto = { printer: PrinterDto; runtimeStatus?: PrinterRuntimeStatusDto; }; type PrinterRuntimeStatusUpdateDto = { state?: PrinterListenerState; updatedAt: string; bufferedBytes?: number | null; drawer1State?: DrawerState | null; drawer2State?: DrawerState | null; }; type PrinterOperationalFlagsUpdateDto = { printerId: string; updatedAt: string; targetState?: PrinterTargetState; isCoverOpen?: boolean; isPaperOut?: boolean; isOffline?: boolean; hasError?: boolean; isPaperNearEnd?: boolean; }; type PrinterStatusUpdateDto = { printerId: string; updatedAt: string; runtime?: PrinterRuntimeStatusUpdateDto; operationalFlags?: PrinterOperationalFlagsUpdateDto; settings?: PrinterSettingsDto; printer?: PrinterDto; }; ## Requests type CreatePrinterRequestDto = { printer: { id: string; displayName: string }; settings: { protocol: "EscPos"; widthInDots: number; heightInDots: number | null; emulateBufferCapacity: boolean; bufferDrainRate: number | null; bufferMaxCapacity: number | null; }; }; type UpdatePrinterRequestDto = { printer: { id: string; displayName: string }; settings: { protocol: "EscPos"; widthInDots: number; heightInDots: number | null; emulateBufferCapacity: boolean; bufferDrainRate: number | null; bufferMaxCapacity: number | null; }; }; type UpdatePrinterOperationalFlagsRequestDto = { isCoverOpen?: boolean; isPaperOut?: boolean; isOffline?: boolean; hasError?: boolean; isPaperNearEnd?: boolean; targetState?: PrinterTargetState; }; type UpdatePrinterDrawerStateRequestDto = { drawer1State?: "Closed" | "OpenedManually"; drawer2State?: "Closed" | "OpenedManually"; }; ## Endpoints ### POST /api/printers Creates a printer. Response: PrinterResponseDto ### PUT /api/printers/{id} Updates printer metadata and settings. Response: PrinterResponseDto ### GET /api/printers Lists full printer snapshots. Response: PrinterResponseDto[] ### GET /api/printers/{id} Reads a full printer snapshot. Response: PrinterResponseDto ### PATCH /api/printers/{id}/operational-flags Partial update of operational flags. Setting targetState starts or stops the printer listener. Response: PrinterOperationalFlagsDto ### PATCH /api/printers/{id}/drawers Manual drawer open/close (OpenedByCommand cannot be set via API). Response: PrinterRuntimeStatusDto ## Streaming ### GET /api/printers/sidebar Returns sidebar snapshots (metadata + state-only runtime). Response: PrinterSidebarSnapshotDto[] ### GET /api/printers/sidebar/stream Server-sent events for sidebar updates. Event: sidebar Payload: PrinterSidebarSnapshotDto ### GET /api/printers/{id}/runtime/stream Server-sent events for active printer updates. Payload is partial; only changed sections are present (runtime/operationalFlags/settings/printer). Event: status Payload: PrinterStatusUpdateDto ## Error Responses Errors use ProblemDetails-like JSON: type ProblemDetails = { status: number; detail: string; instance: string; }; --- # Troubleshooting Start from the symptom. | What you see | Page | |---|---| | Connection refused, or reset the moment it opens | [Connection refused or reset](/docs/troubleshooting/connection-refused) | | Nothing appears after printing | [Nothing appears](/docs/troubleshooting/nothing-appears) | | Junk documents nobody printed, from strangers or port scanners | [Documents you did not print](/docs/troubleshooting/unexpected-documents) | | The document has a red border | [Document marked with an error](/docs/troubleshooting/document-has-errors) | | "No visual elements detected" | [Empty documents](/docs/troubleshooting/no-visual-elements) | | Text is there but the characters are wrong | [Garbled characters](/docs/troubleshooting/garbled-characters) | Two things are worth knowing before you start, because they explain most surprises: - **Every printer port is open to the internet.** That is the point of the service, and it is why the [IP whitelist](/#settings/whitelist) is on by default - which also makes it the first thing to check when a connection is rejected or a document never arrives. - **[Workspace Settings -> Access Log](/#settings/connections) records every connection**, allowed and rejected, so you rarely have to guess which of the two happened. - **A document is finished when you close the connection**, or after a short idle timeout. An application that holds the socket open forever never produces one. ## Still stuck? Email [support@virtual-printer.online](mailto:support@virtual-printer.online) with the printer's protocol and port, what your application sent, and what you expected to see. --- # Connection Refused or Reset The two failures look similar in a log and mean opposite things. Check which one you actually get. ## Connection reset immediately after connecting The listener accepted you and then dropped the socket. That is the **IP whitelist** doing its job: the address you connected from is not on the workspace's list, so the connection is reset rather than politely closed. This is the expected first experience from a second machine, because a new workspace starts with the whitelist **enabled** and only the IP you created it from on the list. A CI runner, a POS terminal, or your laptop on a different network is a different address. To fix it: 1. Open [Workspace Settings -> IP Whitelist](/#settings/whitelist). 2. Add the address, one per line. Exact IPs and CIDR ranges both work (`203.0.113.7`, `203.0.113.0/24`), `#` starts a comment. 3. Save, and print again. If you do not know the address to add, [Workspace Settings -> Access Log](/#settings/connections) lists every connection the workspace received, rejected ones included, so you can copy the address straight from there. Turning the whitelist off entirely also works, and is sometimes what you want for a throwaway workspace - but read [Documents you did not print](/docs/troubleshooting/unexpected-documents) first, because the port is reachable from the whole internet. ## Connection refused Nothing is listening. In order of likelihood: 1. **The printer is stopped.** Its card shows the state; start it from the printer's card or the Operations panel. 2. **Wrong port.** Each printer has its own; the card shows it, and so does `settings.tcpListenPort` from the API. See [Raw TCP port 9100](/docs/start/raw-tcp-port-9100). 3. **Wrong host.** `virtual-printer.online` for the hosted service, your own host if you self-host. ## Connection times out Nothing answers at all, which is a network problem rather than a printer problem. Printer ports are non-standard, and plenty of corporate networks allow outbound 80 and 443 only. If it works from home and hangs at the office, that is the cause - ask for the port to be opened, or self-host inside the network. To separate the two cases quickly: ```bash # Linux / macOS nc -vz virtual-printer.online 9411 # Windows PowerShell Test-NetConnection virtual-printer.online -Port 9411 ``` ## Still stuck? Email [support@virtual-printer.online](mailto:support@virtual-printer.online) with the printer's protocol and port, what your application sent, and what you expected to see. --- # Nothing Appears Your application wrote its bytes and the document list stays empty. ## Start here: the IP whitelist rejected you This is the most common cause, and it is silent from the outside - the connection is accepted and then reset, so a client that does not check for errors looks like it printed successfully. Open [Workspace Settings -> Access Log](/#settings/connections). It lists every connection the workspace received and whether each was allowed, so a rejected attempt from your address is visible immediately, with the address to add. If your attempt is listed as rejected, add that address in [Workspace Settings -> IP Whitelist](/#settings/whitelist) - see [Connection refused or reset](/docs/troubleshooting/connection-refused) for the details. New workspaces start with the whitelist enabled and only the IP they were created from on it, so any second machine - a CI runner, a POS terminal, your laptop on another network - is rejected until you add it. If your attempt is not in the log at all, the bytes never reached the service: the problem is the address you are connecting to or the network path, not the workspace. ## The connection is still open A document is finalized when the connection **closes**, or after a short idle timeout once the bytes stop arriving. An application that opens a socket at startup and keeps it forever produces nothing you can look at until it closes. Close the socket after each receipt - which is what a real printer expects too - or wait out the idle timeout. ## The printer is stopped A stopped printer is not listening at all, so this usually shows up as [connection refused](/docs/troubleshooting/connection-refused) rather than silence. Check the state on the printer's card; start it, then print again. ## The bytes went somewhere else With several printers in a workspace it is easy to send to the port of one and watch the documents of another. Compare the port your application is configured with against the port on the card of the printer you are looking at. ## The page is not refreshing Documents arrive over a live stream, but a page left open through a lost connection can stop updating. Reload it. If the document is there after a reload, nothing was lost - it was only the view. ## Still stuck? Email [support@virtual-printer.online](mailto:support@virtual-printer.online) with the printer's protocol and port, what your application sent, and what you expected to see. --- # Documents You Did Not Print If documents appear that you did not send - junk text, fragments of someone else's receipt, or the output of a port scanner - your printer port has been found by someone else. That is not a break-in. **Every printer port is reachable from the whole internet**, by design: it is what lets a POS terminal or a CI runner anywhere print to it. Ports in the 9100 block are also scanned constantly, because that is where network printers live. ## The fix: the IP whitelist Open [Workspace Settings -> IP Whitelist](/#settings/whitelist), switch it on, and list the addresses allowed to print: - one entry per line, exact IP or CIDR range (`203.0.113.7`, `203.0.113.0/24`); - `#` starts a comment, so you can label which entry is which; - connections from anywhere else are reset the moment they open. New workspaces start with this **already enabled**, holding the IP you created the workspace from. If strangers are printing into yours, the whitelist has been turned off or widened at some point. [Workspace Settings -> Access Log](/#settings/connections) shows what has been connecting lately, allowed and rejected alike - useful for confirming the whitelist now does what you expect. ## What this does not protect The whitelist controls who may connect. It does not encrypt anything: raw TCP is unencrypted by definition, so whatever you print travels in the clear either way. Keep real customer data out of the hosted service - see [Security guidelines](/docs/policies/security) and [Choosing a test printer](/docs/concepts/choosing-a-test-printer). ## Still stuck? Email [support@virtual-printer.online](mailto:support@virtual-printer.online) with the printer's protocol and port, what your application sent, and what you expected to see. --- # Document Marked With an Error A document drawn with a **red border** contains at least one error the parser recorded while reading your byte stream. The rest of the document still renders; the border marks that something in it did not add up. ## Reading the error - A **warning triangle** appears next to the document. Hover it to see the error text. - Or switch on **Raw Data** for that document to see the byte stream alongside the parsed elements, with the failure in place. This is the faster route when you need to know *where* in the receipt it went wrong rather than just what happened. Most messages are self-explanatory: an argument outside the range the command allows, a length that does not match the data that followed, a command cut short by the connection closing mid-sequence. ## When it is your byte stream Common causes, in order: - **A truncated job.** The connection closed in the middle of a command. Usually an application that times out or exits without flushing. - **An out-of-range argument.** For example a colour byte where the command only allows 0/48 and 1/49. - **Wrong protocol.** EPL sent to an ESC/POS printer produces a document full of errors rather than a label. See [Printer protocols](/docs/protocols). ## When it is us The parser may also flag a command it does not implement yet. ESC/POS is large, vendors extend it, and some sequences are undocumented - so an error does not automatically mean your client is wrong. If the command looks correct against the specification, **write to [support@virtual-printer.online](mailto:support@virtual-printer.online)** with the document and the bytes. Adding a command is usually quick, and a real byte stream from the field is exactly what makes that possible. --- # Empty Documents A document that renders as **"No visual elements detected"** was received in full and contained nothing to draw. That is often correct behaviour rather than a fault. ## Jobs that legitimately print nothing - Opening the cash drawer connected to the printer. - Polling printer status before continuing a receipt. - Triggering the buzzer, internal or external. - Initialising the printer at application startup. All of these are real jobs a real printer would also perform without moving paper. ## When you expected output - **Check the protocol.** ESC/POS data sent to an EPL printer, or the reverse, yields a document with no elements or a document full of errors. See [Printer protocols](/docs/protocols). - **Switch on Raw Data** for the document. If the bytes are there but nothing was drawn, the commands were understood and simply did not print; if the bytes are not what you expected, the problem is upstream in your application. - **Look for a red border**, which means something failed to parse - see [Document marked with an error](/docs/troubleshooting/document-has-errors). ## Still stuck? Email [support@virtual-printer.online](mailto:support@virtual-printer.online) with the printer's protocol and port, what your application sent, and what you expected to see. --- # Garbled Characters The layout is right, the text is in the right places, and the characters are wrong - accented letters turned into symbols, Cyrillic into line-drawing characters. This is a **code-page mismatch**, and it is the single most common surprise when moving from a driver to raw ESC/POS. ## Why it happens ESC/POS text is bytes, not Unicode. A single byte means whatever the currently selected code page says it means. Your application encodes the text with some code page; the printer decodes it with whichever one it was last told to select. When the two disagree, every non-ASCII character comes out wrong while ASCII looks perfect - which is why the problem often hides until the first accented name. ## The fix Select the code page explicitly at the start of every job, and encode your text with the same one: - `ESC t n` selects the page, where `n` is the printer's identifier for it - `0` for CP437, `17` (`0x11`) for CP866, `46` (`0x2E`) for Windows-1251. - Then encode your strings with that same page in your application, rather than sending UTF-8. Do not rely on the printer's power-on default, and do not assume it survives between jobs. ## Confirming what was sent Switch on **Raw Data** for the document. Each element is shown with its raw bytes next to the parsed command, and text is decoded with the code page in effect at that point - enough to tell a wrong `ESC t` from text that was encoded wrongly before it ever left your application. If the document contains no `ESC t` at all, that is the answer: your application never selected a page. ## Still stuck? Email [support@virtual-printer.online](mailto:support@virtual-printer.online) with the printer's protocol and port, what your application sent, and what you expected to see. --- # Frequently Asked Questions Common questions about testing receipt and label printers without hardware. For a specific symptom, see [Troubleshooting](/docs/troubleshooting); for driving the printer from tests, see [CI/CD](/docs/ci-cd). ## Getting Started ### How do I create a workspace? Click "Create or Access Workspace" on the main page, enter your name (optional), and you'll receive a unique workspace token. Save this token securely for future access. ### How do I access my workspace from another device? Use "Access Workspace" and enter your workspace token. You can use the same workspace on multiple devices simultaneously. ### I lost my workspace token. Can I recover it? Workspace tokens cannot be recovered. You'll need to create a new workspace. Always save your token in a secure location. ## Virtual Printer Setup ### Can I test receipt or label printing without buying a physical printer? Yes! Virtual Printer provides an online receipt and label printer emulator that lets you test ESC/POS commands, thermal printer output, and label printing without any physical hardware. Perfect for POS development and testing printing APIs. ### Does virtual-printer.online offer a public raw TCP port, or only a web/HTTP interface? A public raw TCP port. This is not a browser-only or paste-your-hex-dump emulator: every printer you create listens on a real, always-online TCP socket on `virtual-printer.online`, reachable from the public internet. Point your POS software at `virtual-printer.online:`, write ESC/POS or EPL bytes, and close the connection — exactly what you would do with a physical network thermal printer on port 9100. Nothing to install, no Docker container, no local bridge, and no HTTP wrapper around the print stream. (The HTTP/REST API exists too, but it is for managing printers and reading back the rendered documents — printing itself is raw TCP.) ### Which port does my printer listen on? Its own port from the block that starts at **9100**, shown on the printer's card in the web interface. Ports are handed out in sequence and stay with the printer, so you can hard-code `host:port` in your POS configuration. Full explanation: [Raw TCP port 9100](/docs/start/raw-tcp-port-9100). ### Can I create multiple printers? Yes. You can create multiple virtual printers in one workspace and emulate a real environment for a large site. ### Can I change the printer port? No. Assignment is automatic and sequential from the 9100 block, so you cannot request a specific port — including 9100 itself, which is reserved as the base of the range. You get the next free port, and you keep it for as long as the printer exists. ### Why isn't my virtual receipt printer receiving data? When testing ESC/POS commands or thermal printer output, check that: 1. The virtual printer status shows "Listening" (not "Stopped") 2. Your POS application is connecting to the correct host and port 3. No firewall is blocking the TCP connection 4. You're using the correct protocol (raw TCP, not HTTP) 5. Your receipt printer emulator is configured properly ### How do I delete a printer? Select the printer, open the **Operations** panel on the right, expand **Danger Zone**, and click **Delete Printer**. That removes the printer's documents along with it. The same section has **Delete all documents**, which clears the history but keeps the printer and its port. ## Common Misconceptions ### Is this a browser-based emulator where I paste ESC/POS bytes into a page? No. Pasting a payload into the web UI is a convenience (the **Import document** dialog), not how the service works. The normal path is a raw TCP socket: your application connects to `virtual-printer.online:` and writes bytes, exactly as it would to a physical printer. Parsing and rendering happen on the server — the browser only displays the result, and it does not need to be open while you print. ### Can my application connect to it as if it were a real printer? Yes, and that is the only way to print to it. There is no web page in the print path to work around: the listener is a plain TCP socket speaking the same JetDirect/AppSocket stream a network thermal printer accepts on port 9100. If your POS software can print to `192.168.1.50:9100`, it can print to `virtual-printer.online:` with a configuration change and nothing else. ### Does it exercise my transport, timeouts, and error handling, or only the rendering? All of it. The connection is a real socket, so your connect, write, close, and reconnect paths run unchanged, and a document is finalized when you close the connection or after a short idle timeout. On top of that: - The printer **answers status requests on the socket**: real-time `DLE EOT n`, plus `GS r`, `ESC u`, `ESC v`, `GS I`, and Automatic Status Back. - You can **inject faults** — paper out, paper near end, cover open, offline, error — from the web interface or from a test, and the status the printer reports changes accordingly, so you can check that your app alerts staff instead of dropping the order silently. Stopping the printer lets you test connection refused. - **Buffer capacity and drain rate** can be emulated per printer: a job that outruns the buffer is marked with a buffer overflow, and the printer reports itself busy until the buffer drains. - Cash-drawer kick-out state is emulated as well. ### Does it only speak ESC/POS? ESC/POS receipts and EPL labels are both supported today; TSPL is in development. Each printer is created with the protocol it should speak. ## Documents ### I already have a captured dump - can I just upload it instead of printing? Yes, and a **hex dump works as well as base64**. Select the printer, open the **Operations** panel and click **Import Doc**: drop a binary file, or paste the bytes as hex (`1B 40 48 65 6C 6C 6F 0A`) or as base64. They go through the same parser a printed job goes through. Over the API it is `POST /api/printers/{id}/documents/import` with the bytes in `Data` and an optional `"Format": "Hex" | "Base64"`. One thing to know: hex digits are also base64 characters, so a run-together payload like `1B40` is valid as both and means different bytes in each. That case is **refused** rather than guessed - state `Format`, or paste the dump with its bytes spaced apart, which is recognised as hex. Full details: [Import a captured dump](/docs/start/import-a-dump). ### Can I download documents, or read them programmatically? Yes, over the API - which is how a test suite consumes them: - **`GET /api/printers/{id}/documents/canvas`** returns documents as JSON: every element with its text, position in dots, font and styling, alongside the byte counts, the client address, the protocol and any parse errors. Paged with `limit` and `beforeId`. - **`GET /api/printers/{id}/documents/canvas/stream`** is the same data as a Server-Sent Events stream, one event per completed document, so a test can wait for a receipt instead of polling. - **`GET /api/media/{mediaId}`** returns the image bytes of a logo or raster image inside a document. That is enough to save a receipt to a file, assert on it in a pipeline, or archive it. See [Asserting on a receipt](/docs/ci-cd/assertions) for how tests use this, and the [endpoint reference](/docs/api/endpoints) for the exact shapes. What is not there yet is a save button in the web interface, and an export of the rendered receipt as PNG or PDF. Neither is needed to read documents from code. ### Why aren't my documents appearing? Most often the **IP whitelist** rejected the connection. A new workspace starts with it enabled and only the address it was created from on the list, so a second machine - a CI runner, a POS terminal, your laptop on another network - is reset the moment it connects, which looks like a successful print to a client that does not check for errors. [Workspace Settings -> Access Log](/#settings/connections) shows every connection the workspace received and whether it was allowed; add the rejected address in [IP Whitelist](/#settings/whitelist). The other common cause is a connection your application never closed: a document is finalized when the socket closes, or after a short idle timeout. [Nothing appears](/docs/troubleshooting/nothing-appears) works through the rest in order. ## Workspace & Data ### How long does my workspace last? Workspaces expire **1 year** after the last time they were accessed. What counts as access is *reading*: signing in through the web interface, or fetching at least one document through the REST API. Printing into the workspace on its own does not reset the timer — a printer nobody ever looks at eventually expires with its workspace. ### Can I delete my workspace? Yes. Open Workspace Settings, go to **Danger Zone**, and click **Delete Workspace**. ### Is my data secure? Virtual Printer can operate in two modes: **Cloud Mode (virtual-printer.online):** - Raw TCP connections are **not encrypted** - Data may be intercepted during transmission - **Only use for testing and development** with non-sensitive data - Never use for production workloads **Self-Hosted Mode (local network/machine):** - Install Virtual Printer on your own infrastructure - Security is your administrator's responsibility - Suitable for production use when properly secured - Can be isolated within private networks or VPNs See our [Security](/docs/policies/security) page for detailed information about data transmission and [Privacy Policy](/docs/policies/privacy) for data storage details. ## Still Need Help? For a symptom rather than a question, start with the [troubleshooting guide](/docs/troubleshooting). Otherwise, email us at [support@virtual-printer.online](mailto:support@virtual-printer.online) with: - Description of the issue - Steps to reproduce - Error messages (if any) - Screenshots (if applicable) --- # How It Works ## The path a receipt takes 1. Your application opens a TCP socket to the printer's host and port and writes bytes. 2. The server reads them as a print job and runs them through the parser for that printer's protocol. Every command becomes a **document element**: a run of text with its styling, a barcode, an image, a cut, a drawer pulse, a status reply, or a note that a command was ignored. 3. The job ends when you close the connection, or after a short idle timeout if you leave it open. 4. The elements are stored and rendered. The browser draws the receipt from them, and the API can return the same elements for a test to assert against. Because parsing happens on the server, the browser is a viewer rather than a participant. Nothing depends on a page being open while you print. ## Why elements rather than a picture A rendered image would answer only "does it look right". Elements answer "what did the printer actually receive": which command set the bold, which byte was ignored and why, what status the printer replied with. That is what makes the result useful in an assertion and in a bug report. Every byte a job sends is accounted for — including bytes the printer ignores and commands cut short by a client that disconnected mid-command — so the elements of a document add up to the stream that produced it. ## Emulated hardware, not just a parser A printer here has state, and the state is visible to your application: - Operational flags — paper out, paper near end, cover open, offline, error — set from the web interface or over the API, which change the status the printer reports. - Cash drawer state, driven by ESC/POS pulses or set through the API. - An optional receive buffer with a configurable capacity and drain rate. It fills as bytes arrive and empties at the rate you set; a job that outruns it is marked with a buffer overflow, and the printer reports itself busy or full while it holds data. See [CI/CD and automated tests](/docs/ci-cd) for driving these from test code. --- # Choosing a Test Printer Three different things get called "testing the printer", and they exercise different parts of your system. Picking the wrong one gives you a green test that proves nothing. ## Print-to-PDF or a virtual driver Built into Windows and macOS, free, and useful — **if your application prints through the operating system's print queue**. It exercises your document layout and the driver. It is the wrong tool if your application builds ESC/POS bytes and writes them to a socket, which is what most POS software does. That path never touches the OS driver stack, so a PDF driver tests none of it. ## A raw TCP printer emulator This service, and the local open-source emulators, live here. The emulator accepts the same byte stream a network printer accepts, so your transport, your command generation, your encoding and your error handling all run unchanged. What you give up is the physical layer: nothing tells you the paper is actually in the printer, or that the cutter jams on thick stock. Between hosted and local there is a real trade-off, and it is not always in our favour: - **Hosted (this service)** — nothing to install or maintain, shareable with teammates and reachable from a CI runner without networking work, with history kept for you. In exchange the connection is unencrypted and leaves your network, so it is for test data only. - **Local or self-hosted** — your data never leaves the machine, and it works offline. In exchange you run and maintain it, and sharing a rendered receipt with a colleague takes a screenshot. If you are printing anything resembling real customer data, run it locally. That is the honest answer, and it is why this service is self-hostable. ## Real hardware The only thing that proves paper, cutter, ribbon and print quality. It is also the thing you cannot put in a pipeline: it needs a person, a physical location, and consumables. The practical arrangement most teams land on is an emulator for everything that runs on every commit, and a real printer for a deliberate check before a release. ## Summary | You want to know | Use | |---|---| | Does my document layout look right when printed through the OS? | Print-to-PDF driver | | Do my ESC/POS bytes render correctly, and does my app handle the socket? | Raw TCP emulator | | Does my app react when the paper runs out mid-receipt? | Emulator with fault injection | | Does the cut land in the right place on this specific paper? | Real hardware | --- # Limits and Retention The authoritative numbers are in the [Terms of Service](/docs/policies/terms); this page explains what they mean in practice. ## Documents Documents are deleted automatically once they reach the retention age configured for the workspace — **30 days** by default. A workspace can set its own value, and setting it to zero keeps documents until something else removes them. Deleting a printer removes its documents with it. Retention is about age, not about activity: a document is deleted 30 days after it was printed, whether or not anyone looked at it. ## Workspaces A workspace expires **one year after the last time it was read**. What counts as reading is opening it in the web interface or fetching a document through the API. Printing into it does not count — a workspace that only receives jobs, with nobody ever looking at the result, is not in use by anyone. Practically, if you open your workspace even once a year, it stays. ## What expiry costs you When a workspace goes, its printers go with it, and their **ports are not held for you**. Ports are assigned in sequence and never recycled, so a new printer gets a new number rather than the one you had. If a fixed address matters — a POS terminal configured in the field, a pipeline with the port in its config — that is the reason to keep the workspace alive rather than recreate it. See [Raw TCP port 9100](/docs/start/raw-tcp-port-9100) for how ports are assigned in the first place. ## Per-workspace limits Printers per workspace and total stored documents are capped; the current figures are in the [Terms of Service](/docs/policies/terms). They exist to keep one workspace from crowding out the rest of a shared service, and they are generous for testing. --- # Security Understanding the security implications of Virtual Printer is essential for safe usage. ## Deployment Modes Virtual Printer can operate in two distinct modes with different security considerations: ### Cloud Mode (virtual-printer.online) > **WARNING:** Data transmitted to **virtual-printer.online** uses raw TCP connections without encryption. When using printers hosted on virtual-printer.online: - Raw TCP connections are **not encrypted** - Data may be **intercepted** by third parties on the network - Data may be **modified** through man-in-the-middle attacks - **Only use for testing and development** with non-sensitive data - **Never use for production workloads** ### Self-Hosted Mode (Local Network/Machine) When you host Virtual Printer on your own infrastructure: - Security is your administrator's responsibility - Can be deployed within private networks - Suitable for production use when properly secured - Can be isolated using VPNs or network segmentation - You control encryption and access policies ## Recommended Usage ### Safe Use Cases (Cloud Mode) Appropriate for cloud-hosted virtual-printer.online: - Local development and testing - Debugging print functionality - Demo and presentation environments - Non-sensitive test data only ### Unsafe Use Cases (Cloud Mode) **Do not use cloud mode for:** - Printing customer data - Processing payment receipts - Handling personal information - Production environments - Any sensitive or confidential data ### Production Deployment (Self-Hosted Only) For production use, you must self-host Virtual Printer: - Deploy on your local network (not public cloud) - Use VPN for remote access if needed - Never expose TCP endpoints to the public internet - Implement proper network security and access controls - Follow your organization's security policies ## Questions? If you have security questions or concerns, please contact us at [support@virtual-printer.online](mailto:support@virtual-printer.online). --- # Privacy Policy This privacy policy explains how Virtual Printer collects, uses, and protects your information. ## Information We Collect ### Data You Provide - **Workspace name** (optional): Display name for your workspace - **Print documents**: ESC/POS data you send to virtual printers - **Printer configurations**: Names, settings, and preferences ### Data We Collect Automatically - **Workspace tokens**: Generated identifiers for authentication - **IP addresses**: Collected in server logs for security and diagnostics - **Usage data**: Timestamps, document counts, printer activity - **Browser information**: User-Agent headers for compatibility ### Data We Do NOT Collect - Email addresses - Payment information - Personal identification beyond optional workspace names ## How We Use Your Information We use collected data to: - Provide the printer emulation service - Display documents in your workspace - Maintain service functionality and performance - Troubleshoot technical issues - Improve the service We do **not**: - Sell your data to third parties - Use your data for advertising - Share your data with external services (except infrastructure providers) ## Data Storage and Security ### Storage - Data is stored on secure cloud servers - Workspace tokens are cryptographically hashed - Documents are stored in their original ESC/POS format ### Retention - **Documents**: Automatically deleted after **30 days** - **Workspaces**: Expire **1 year** after last access (a web sign-in or reading a document through the REST API resets the timer) - **Expired data**: May be permanently deleted without notice ### Security Measures - Hashed workspace tokens - Secure server infrastructure - Regular security updates ## Important Security Limitation > **WARNING:** Data transmission to virtual-printer.online printers is NOT encrypted. When using printers hosted on virtual-printer.online, print data sent over raw TCP connections: - Travels unencrypted over the network - May be intercepted by third parties - Should never contain sensitive information **Self-hosted deployments:** If you host Virtual Printer on your own infrastructure, data transmission security is your administrator's responsibility. See our [Security](/docs/policies/security) page for detailed guidance on safe usage and deployment modes. ## Your Rights You have the right to: - **Access** your workspace data at any time - **Delete** your workspace and all associated data - **Export** your documents through the API (`GET /api/printers/{id}/documents/canvas`) To exercise these rights, email [support@virtual-printer.online](mailto:support@virtual-printer.online). ## Data Sharing We do not share your data with third parties except: - **Infrastructure providers** (cloud hosting, required for service operation) - **Legal requirements** (if required by law or legal process) We do not use third-party analytics, advertising, or tracking services. ## International Data Transfers Data may be stored and processed in data centers located in various countries. By using Virtual Printer, you consent to international data transfers. ## Children's Privacy Virtual Printer is not intended for users under 13 years of age. We do not knowingly collect information from children. ## Changes to This Policy We may update this privacy policy from time to time. Continued use of the service after changes constitutes acceptance of the new policy. ## Contact Privacy questions or concerns? Email [support@virtual-printer.online](mailto:support@virtual-printer.online). ## Your Consent By using Virtual Printer, you consent to this privacy policy and our [Terms of Use](/docs/policies/terms). --- # Terms of Use By using Virtual Printer, you agree to these terms of service. ## Service Description Virtual Printer provides cloud-based printer emulation for testing and development purposes. The service is provided "as is" without warranties of any kind. ## Acceptable Use You may use Virtual Printer to: - Test and develop printing functionality - Debug ESC/POS command sequences - Demonstrate printing capabilities - Integrate with development and staging environments You **must not** use Virtual Printer to: - Print sensitive, confidential, or personal data - Process production workloads - Print customer information or payment data - Violate any applicable laws or regulations - Attempt to access other users' workspaces - Interfere with service operation or security ## Service Limits Each workspace is subject to the following limits: | Resource | Limit | |----------|-------| | Printers per workspace | 10 | | Total documents per workspace | 10,000 | | Document retention | 30 days | | Workspace expiration | 1 year after last access | We reserve the right to adjust these limits or enforce them to maintain service quality. ## Security Notice > **WARNING:** Virtual Printer uses unencrypted raw TCP connections. Data transmitted to virtual printers: - Is **not encrypted** - May be **intercepted or modified** in transit - Should **never** contain sensitive information See our [Security](/docs/policies/security) page for detailed guidance. ## Account Termination We reserve the right to suspend or terminate access to Virtual Printer for: - Violation of these terms - Abusive behavior or excessive resource usage - Security concerns - Any reason at our discretion ## Data Retention - Documents are automatically deleted after 30 days - Workspaces expire **1 year** after the last time they were accessed — signing in through the web interface or reading at least one document through the REST API resets the timer; receiving print jobs alone does not - Expired data may be permanently deleted without notice See our [Privacy Policy](/docs/policies/privacy) for complete data handling details. ## Disclaimer of Warranties Virtual Printer is provided "as is" and "as available" without warranties of any kind, either express or implied, including but not limited to: - Merchantability - Fitness for a particular purpose - Non-infringement - Uninterrupted or error-free service ## Limitation of Liability In no event shall Virtual Printer be liable for any indirect, incidental, special, consequential, or punitive damages, including but not limited to: - Loss of data - Loss of profits - Business interruption - Damages resulting from use or inability to use the service ## Service Availability We strive to provide reliable service but do not guarantee: - Continuous availability - Specific uptime percentages - Data backup or recovery The service may be interrupted for maintenance, updates, or unforeseen technical issues. ## Changes to Terms We may modify these terms at any time. Continued use of the service after changes constitutes acceptance of the new terms. ## Contact Questions about these terms? Email [support@virtual-printer.online](mailto:support@virtual-printer.online). ## Third-Party Licenses Virtual Printer uses third-party software components. Each component is licensed under the terms specified by its respective copyright holders. For a complete list of third-party components and their licenses, see our [Third-Party Licenses](/docs/policies/licenses) page. --- # Licenses Virtual Printer incorporates the following third-party software components. Each component is licensed under the terms specified by its respective copyright holders. ## Project License The application source code and repository content are licensed under the MIT License, except for the bundled `VirtualPrinterMono-*.ttf` font files. The VirtualPrinterMono fonts may be used and redistributed only as an unmodified part of virtual-printer.online, including forks and self-hosted deployments. They may not be reused in another project. See the [VirtualPrinterMono Font License](/assets/fonts/LICENSE.txt) for the complete terms. ## Table of Contents - [.NET Runtime Components](#net-runtime-components) - MIT License - [Application Libraries](#application-libraries) - MIT License, Apache-2.0 - [UI Assets](#ui-assets) - MIT License --- ## .NET Runtime Components The following Microsoft .NET packages are used under the **MIT License**: | Package | Version | License | License Text | |---------|---------|---------|--------------| | Microsoft.AspNetCore.Authentication.JwtBearer | 8.0.* | MIT | [aspnetcore-mit-license.txt](/assets/licenses/aspnetcore-mit-license.txt) | | Microsoft.Data.Sqlite | 8.0.10 | MIT | [efcore-mit-license.txt](/assets/licenses/efcore-mit-license.txt) | | Microsoft.EntityFrameworkCore | 8.0.10 | MIT | [efcore-mit-license.txt](/assets/licenses/efcore-mit-license.txt) | | Microsoft.EntityFrameworkCore.Sqlite | 8.0.10 | MIT | [efcore-mit-license.txt](/assets/licenses/efcore-mit-license.txt) | | Microsoft.Extensions.Configuration | 8.0.0 | MIT | [runtime-mit-license.txt](/assets/licenses/runtime-mit-license.txt) | | Microsoft.Extensions.Configuration.Binder | 8.0.0 | MIT | [runtime-mit-license.txt](/assets/licenses/runtime-mit-license.txt) | | Microsoft.Extensions.Configuration.EnvironmentVariables | 8.0.0 | MIT | [runtime-mit-license.txt](/assets/licenses/runtime-mit-license.txt) | | Microsoft.Extensions.Configuration.Json | 8.0.0 | MIT | [runtime-mit-license.txt](/assets/licenses/runtime-mit-license.txt) | | Microsoft.Extensions.Hosting.Abstractions | 8.0.0 | MIT | [runtime-mit-license.txt](/assets/licenses/runtime-mit-license.txt) | | Microsoft.Extensions.Options | 8.0.0 | MIT | [runtime-mit-license.txt](/assets/licenses/runtime-mit-license.txt) | | System.IdentityModel.Tokens.Jwt | 8.14.0 | MIT | [azuread-identitymodel-mit-license.txt](/assets/licenses/azuread-identitymodel-mit-license.txt) | | System.Text.Encoding.CodePages | 10.0.0-rc.2.25502.107 | MIT | [runtime-mit-license.txt](/assets/licenses/runtime-mit-license.txt) | --- ## Application Libraries ### BSD-2-Clause License | Package | Version | Description | Link | License Text | |---------|---------|-------------|------|--------------| | **Markdig** | 0.38.0 | Fast, extensible Markdown processor for .NET | [GitHub](https://github.com/xoofx/markdig) | [markdig-bsd-2-clause-license.txt](/assets/licenses/markdig-bsd-2-clause-license.txt) | ### MIT License | Package | Version | Description | Link | License Text | |---------|---------|-------------|------|--------------| | **SkiaSharp** | 2.88.8 | Skia graphics library for .NET | [GitHub](https://github.com/mono/SkiaSharp) | [skiasharp-mit-license.txt](/assets/licenses/skiasharp-mit-license.txt) | | **Swashbuckle.AspNetCore** | 6.6.2 | Swagger/OpenAPI tooling for ASP.NET Core | [GitHub](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) | [mit-license.txt](/assets/licenses/mit-license.txt) | ### Apache License 2.0 | Package | Version | Description | Link | License Text | |---------|---------|-------------|------|--------------| | **CodeGlyphX** | 1.4.0 | Zero-dependency QR and barcode toolkit for .NET | [GitHub](https://github.com/EvotecIT/CodeGlyphX) | [apache-2.0-license.txt](/assets/licenses/apache-2.0-license.txt) | | **FluentValidation** | 12.0.0 | Small validation library for .NET | [GitHub](https://github.com/FluentValidation/FluentValidation) | [fluentvalidation-apache-2.0-license.txt](/assets/licenses/fluentvalidation-apache-2.0-license.txt) | | **Mediator.Net** | 4.9.0 | In-process mediator with CQRS support | [GitHub](https://github.com/mayuanyang/Mediator.Net) | [apache-2.0-license.txt](/assets/licenses/apache-2.0-license.txt) | | **Mediator.Net.MicrosoftDependencyInjection** | 4.9.0 | Mediator.Net DI integration for Microsoft.Extensions.DependencyInjection | [GitHub](https://github.com/mayuanyang/Mediator.Net) | [apache-2.0-license.txt](/assets/licenses/apache-2.0-license.txt) | | **ZXing.Net** | 0.16.11 | Barcode encoding/decoding library | [GitHub](https://github.com/micjahn/ZXing.Net) | [zxing-net-apache-2.0-license.txt](/assets/licenses/zxing-net-apache-2.0-license.txt) | | **ZXing.Net.Bindings.SkiaSharp** | 0.16.12 | SkiaSharp binding for ZXing.Net | [GitHub](https://github.com/micjahn/ZXing.Net) | [zxing-net-apache-2.0-license.txt](/assets/licenses/zxing-net-apache-2.0-license.txt) | --- ## UI Assets ### MIT License | Asset | Description | Link | License Text | |-------|-------------|------|--------------| | **Feather Icons** | Open source icon set | [feathericons.com](https://feathericons.com/) | [feather-icons-mit-license.txt](/assets/licenses/feather-icons-mit-license.txt) | --- For questions about licensing, please contact [support@virtual-printer.online](mailto:support@virtual-printer.online).