paperminder

n8n node for PaperMinder thermal printers

Package Information

Released: this month
Downloads: 0 weekly / 0 monthly
Latest Version: 0.1.2
Author: NumenMirus

Available Nodes

Documentation generating…

Documentation

n8n-nodes-paperminder

An n8n community node for PaperMinder printers. It exposes four operations you can drop into any n8n workflow: list the printers your account can reach, and send text, QR codes, or barcodes to a chosen printer. Send operations carry the full PaperMinder status payload back into the workflow — including the offline-accept case — so downstream nodes can branch on whether the job actually left the server or was queued for an offline printer.

Prerequisites

  • Node.js ≥ 24 — n8n 2.x declares engines.node >= 24.0.0, and this node targets the same runtime.
  • Docker — only required if you want to spin up the throwaway test instance under deploy/docker-compose.test.yml. The node itself installs and runs like any other n8n community node.

Getting an API key

  1. Mint a key with POST /api/users/me/api-keys, sending a body like {"name": "n8n", "scopes": ["printers:read", "messages:send"], "expires_in_days": null}.
  2. The server returns the raw key once in the key field (format: pm_live_<43-char-urlsafe>). Store it immediately — PaperMinder does not display it again.
  3. The two scopes above are the only ones this node ever requests; nothing more is needed.

Credential fields

Create a PaperMinder API credential in n8n and supply:

  • API Key — your pm_live_… key. The field is password-masked.
  • Base URL — defaults to https://api.paperminder.org. Change it only if you point at a self-hosted PaperMinder deployment. Trailing slashes are tolerated; non-HTTP(S) values are rejected.

n8n verifies the credential by calling GET /api/auth/me against the configured base URL. That endpoint accepts any pm_live_ key regardless of scope, so a send-only key still passes the test step.

Operations

Resource: Printer → List

No additional parameters.

Calls GET /api/printers?scope=accessible with scope printers:read. Emits one output item per printer; an empty list emits zero items. The json of each item is the printer object verbatim — id, uuid, name, location, online, ws_connected, firmware_version, last_connected, last_ip, claimed, auto_update, update_channel, platform, user_uuid, created_at — no fields are projected out.

Resource: Message → Send Text

Field Type Notes
Printer resource locator From list (backed by searchPrinters) or By ID with a UUID regex
Message string, required, max 500 chars Trimmed by the server

Calls POST /api/message with { recipient_id, message }.

Output json (printer online, HTTP 200):

{ "status": "sent" }

Output json (printer offline, HTTP 202):

{ "status": "accepted_for_processing" }

Resource: Message → Send QR Code

Field Type Notes
Printer resource locator Same modes as above
URL string, required, max 2048 chars PaperMinder may shorten the URL before encoding
Title (additional field) string, ≤128 chars Omitted from the body when empty
Description (additional field) string, ≤500 chars Omitted from the body when empty

Calls POST /api/message/qrcode with { recipient_id, url, …title/description if non-empty }.

Output json (printer online):

{ "status": "sent", "url_shortened": true, "final_url": "https://pm.to/abc123" }

url_shortened and final_url are present whenever the server shortened the URL; otherwise they are absent. Never assume final_url equals the URL you sent.

Output json (printer offline):

{ "status": "accepted_for_processing", "url_shortened": true, "final_url": "https://pm.to/abc123" }

Resource: Message → Send Barcode

Field Type Notes
Printer resource locator Same modes as above
Data string, required, max 255 chars Trimmed; per-type character rules apply
Barcode Type options, default CODE128 UPC-A, EAN-13, EAN-8, Code 39, ITF, Codabar, Code 128
HRI Position options, default Below None, Above, Below, Both
Title / Description strings ≤128 / ≤500 chars; omitted from the body when empty

Calls POST /api/message/barcode with { recipient_id, data, barcode_type, hri_position, …title/description if non-empty }. The wire format uses snake_case (barcode_type, hri_position) regardless of the UI parameter names.

Barcode data validation runs before any network call and mirrors the server's own rules — see docs/api-contract.md for the per-type character set and length rules.

Output json (printer online):

{ "status": "sent" }

Output json (printer offline):

{ "status": "accepted_for_processing" }

Important: offline accept is not a print confirmation

accepted_for_processing means the PaperMinder server queued the job for an offline printer — it is not proof that the message printed. A printer reported as offline will receive the job once it reconnects, but PaperMinder offers no job ID and no delivery callback, so there is no way to confirm printing from inside the workflow.

This node never automatically retries a send. PaperMinder exposes no idempotency key, so a retried send after a timeout could print the same job twice. If you re-run a workflow that sent to an offline printer, expect duplicate output when the printer reconnects.

Expressions

Pipe fields from previous nodes directly into the printer and message fields. The Printer field's By ID mode is the easiest place to use an expression:

Printer (By ID) : ={{ $json.printerUuid }}
Message         : ={{ $json.subject }}

$json.printerUuid must be a UUID string; the field validates it against ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$. Other fields accept any string expression — n8n resolves them at run time.

Why programmatic, not declarative

This node uses an imperative execute() rather than n8n's declarative routing for four concrete reasons:

  1. Pre-flight validation must run before any network call. Barcode data must be checked against the per-type character set, and message / URL / data must be length-checked. A declarative node has no hook to fail a request before it goes out; a programmatic one does.
  2. Sanitized error mapping for 401 / 403 / 404 / 422 / 429 / timeouts needs code-level NodeApiError construction — flattening the FastAPI detail payload, attaching the rate-limit headers where present, and stripping the Authorization header before any error reaches the workflow UI.
  3. The 200-vs-202 status pair must be passed through verbatim. A successful send to a live printer is HTTP 200; an offline-printer accept is HTTP 202, with the same body shape. Declarative routing flattens both into a single "success" outcome; the code path here keeps the distinction.
  4. The node is directly unit-testable with a stubbed IExecuteFunctions: a declarative node's routing can only be exercised through n8n's runtime, while this node's operations take a single item index and return an array of items, which is trivial to drive from Vitest.

See docs/api-contract.md for the full HTTP contract this implementation targets.

Deployment

See deploy/README.md for the Docker Compose deployment and rollback procedure.

Discussion