Localhost print agent for Farmora cashier PCs. The Farmora frontend talks to this app over http://127.0.0.1 to configure printers and silently print invoices. Cashiers run FarmoraTray.exe with no console window. A system tray icon shows that the agent is running.
On-site install & setup (for field developers): docs/ON-SITE-SETUP.md
Frontend integration spec: docs/FRONTEND.md
Backend print PDF / ESC-POS spec (for farmora-backend agents): docs/BACKEND-PRINT.md
- Stack: ASP.NET Core / .NET 10, Windows only
- Default URL:
http://127.0.0.1:9123 - Tray: right-click the icon for status, Copy API key, Open config folder, and Exit. Printer mapping stays in the Farmora frontend (or
curl) via config APIs.
cd src/FarmoraTray
dotnet runOn first start, Farmora Tray writes %LocalAppData%\FarmoraTray\config.json and shows a tray balloon that an API key was created. Right-click the tray icon and choose Copy API key. Paste that key into the Farmora frontend printer settings for this PC.
Keep Farmora Tray running while cashiers use Farmora. For production PCs, start FarmoraTray.exe at user logon with Task Scheduler or the Startup folder. Do not install this tray build as a Windows Service. Session 0 has no tray icon. A Windows Service and an installer can come later.
cd src/FarmoraTray
dotnet publish -c Release -r win-x64 --self-contained true -o .\publish\win-x64
dotnet publish -c Release -r win-x86 --self-contained true -o .\publish\win-x86Release publish uses the csproj single-file defaults. Each RID output is primarily FarmoraTray.exe. A .pdb and small json files may sit beside it. Zip the exe per RID as FarmoraTray-win-x64.zip and FarmoraTray-win-x86.zip. The PC does not need .NET installed.
Use the x64 zip on 64-bit Windows and the x86 zip on 32-bit Windows. Check Settings → System → About → System type. Do not install the x86 build on 64-bit Windows as the default path.
To publish a folder of assemblies instead, use Debug or pass /p:PublishSingleFile=false.
| Rule | Behavior |
|---|---|
| Bind address | 127.0.0.1 only |
| API key | Required header X-Farmora-Tray-Key on all routes except GET /health |
| Origin | If Origin is sent and allowedOrigin is set, it must match. If allowedOrigin is empty, any origin is allowed (bootstrap) |
%LocalAppData%\FarmoraTray\config.json
{
"apiKey": "...",
"allowedOrigin": "https://app.farmora.example",
"port": 9123,
"printers": {
"dotMatrix": "EPSON LX-310",
"thermal": "POS-80"
}
}Changing port requires restarting Farmora Tray.
Legacy per-document printer keys (saleOrderDotMatrix, etc.) are migrated into dotMatrix / thermal on load.
Base: http://127.0.0.1:9123
GET /healthNo API key. Response:
{ "status": "ok", "version": "1.0.0.0" }GET /printers
X-Farmora-Tray-Key: <key>{ "printers": ["EPSON LX-310", "Microsoft Print to PDF", "POS-80"] }GET /config
X-Farmora-Tray-Key: <key>PUT /config
X-Farmora-Tray-Key: <key>
Content-Type: application/json{
"allowedOrigin": "https://app.farmora.example",
"printers": {
"dotMatrix": "EPSON LX-310",
"thermal": "POS-80"
}
}GET /config never returns the API key.
| Method | Path | Body | Config key |
|---|---|---|---|
POST |
/dotmatrix |
printers.dotMatrix |
|
POST |
/thermal |
ESC/POS raw | printers.thermal |
Use /dotmatrix for B2B SO, sales return, PO, purchase return (any form PDF).
Use /thermal for retail/thermal receipts.
PDF endpoint accepts either:
Content-Type: application/pdfwith raw PDF bytes, orContent-Type: application/jsonwith{ "pdfBase64": "..." }
Thermal accepts either:
Content-Type: application/octet-streamwith raw ESC/POS bytes, orContent-Type: application/jsonwith{ "rawBase64": "..." }
Success: 204 No Content
Errors:
| Status | When |
|---|---|
400 |
Missing / invalid payload |
401 |
Bad or missing API key |
403 |
Origin not allowlisted |
404 |
Printer not configured or not installed |
503 |
Spooler / driver print failure |
$key = "<paste-from-first-run-log>"
$headers = @{ "X-Farmora-Tray-Key" = $key }
Invoke-RestMethod http://127.0.0.1:9123/printers -Headers $headers
$body = @{
allowedOrigin = "http://localhost:3000"
printers = @{
dotMatrix = "Microsoft Print to PDF"
thermal = "POS-80"
}
} | ConvertTo-Json
Invoke-RestMethod http://127.0.0.1:9123/config -Method Put -Headers $headers -ContentType "application/json" -Body $body$bytes = [System.Text.Encoding]::ASCII.GetBytes("Hello from Farmora Tray`n")
Invoke-WebRequest http://127.0.0.1:9123/thermal `
-Method Post `
-Headers $headers `
-ContentType "application/octet-stream" `
-Body $bytesconst TRAY = "http://127.0.0.1:9123";
const key = localStorage.getItem("farmoraTrayKey");
async function printThermal(rawBytes) {
const res = await fetch(`${TRAY}/thermal`, {
method: "POST",
headers: {
"X-Farmora-Tray-Key": key,
"Content-Type": "application/octet-stream",
},
body: rawBytes,
});
if (!res.ok) throw new Error(await res.text());
}
async function printDotMatrixPdf(pdfBytes) {
const res = await fetch(`${TRAY}/dotmatrix`, {
method: "POST",
headers: {
"X-Farmora-Tray-Key": key,
"Content-Type": "application/pdf",
},
body: pdfBytes,
});
if (!res.ok) throw new Error(await res.text());
}- Detect tray:
GET /health(no key). - Store API key per PC in
localStorage(paste once from Copy API key / support sheet). - Settings page:
GET /printers→ two dropdowns (dot matrix / thermal) →PUT /config(includeallowedOrigin). - After sale/purchase success: generate PDF or ESC/POS in Farmora backend/frontend, then
POST /dotmatrixorPOST /thermal.
Farmora Tray does not generate invoice layouts; it only routes ready-to-print payloads to the configured Windows printers.
PDF jobs use the Windows printto shell verb (Edge / Acrobat / whatever is registered for .pdf). Thermal jobs send raw ESC/POS bytes through the Windows spooler.
dotnet runinsrc/FarmoraTray(or startFarmoraTray.exe)- Copy the API key from the tray menu (Copy API key)
GET /printersandPUT /configmappingdotMatrix/thermalPOST /dotmatrixwith a small PDF — e.g. Microsoft Print to PDF- When a thermal printer is available,
POST /thermalwith ESC/POS bytes