Skip to content

Connector protocol ​

Everything between the browser, the server and the agent. Keep this page and the connector's src/protocol.ts in step when either side changes.

Localhost API (browser → agent) ​

Plain HTTP on 127.0.0.1:41780 (printing.local_agent_url). Every response carries CORS headers and Access-Control-Allow-Private-Network: true, which Chrome requires for a public page calling the loopback address.

Until the agent is paired any origin may call it. Once paired, only the paired server's origin, plus the allowedOrigins in the agent's config, is accepted; other origins get 403.

MethodPathBodyAnswer
GET/status{ name, version, hostname, platform, paired, serverUrl, agentId, agentName, connected }
GET/printers{ printers: [{ name, displayName, isDefault }] }
POST/pair{ serverUrl, token, agentId }The new status. 409 if already paired, 422 on a bad body, 502 if the server refused the token.

The browser side is resources/js/Hooks/useLocalPrintAgent.ts.

Pairing sequence ​

  1. POST {shop}/settings/printers/agents (RegisterPrintAgent) with the name, hostname and platform read from /status. Answers { agent: PrintAgentData, token, serverUrl }. This is the only time the token is legible; the database keeps token_hash = sha256(token).
  2. POST http://127.0.0.1:41780/pair with { serverUrl, token, agentId: agent.uuid }.
  3. The agent immediately calls POST /print-agent/heartbeat to validate the token. If that fails the pairing is not saved and /pair answers 502 with the server's message.

Server API (agent → server) ​

Prefix /print-agent, registered in routes/printAgent.php under the print-agent middleware group: throttle:print-agent (120/min, keyed by token), AuthenticatePrintAgent, SubstituteBindings. Every request carries Authorization: Bearer <token>; a bad token is 401.

MethodPathBodyAnswer
POST/heartbeat{ hostname, platform, arch, version, printers: [{ name, display_name, is_default }] }{ agent: { id, name, shop: { id, name } }, websocket: { key, host, port, scheme, channel } }
POST/broadcasting/auth{ socket_id, channel_name }The Pusher auth signature. 403 unless channel_name is the agent's own channel.
GET/jobs/pending{ jobs: [PrintJobPayload] } — status sent, sent within the delivery window, oldest first.
GET/jobs/{uuid}/fileThe PDF (application/pdf). 404 for another agent's job or a deleted file.
POST/jobs/{uuid}/status{ status: received | printed | failed, error? }204. Terminal jobs ignore late reports.

PrintJobPayload is { id, printer, copies, document, file_name } (PrintJob::toAgentPayload()).

The websocket block is read from the default broadcasting connection. When Reverb is reachable by the shop's computers on a different address than the one the application uses, override it with PRINTING_WEBSOCKET_HOST, PRINTING_WEBSOCKET_PORT and PRINTING_WEBSOCKET_SCHEME.

WebSocket (server → agent) ​

The agent speaks the Pusher protocol to Reverb (wss://{host}:{port}/app/{key}?protocol=7) and subscribes to private-print-agents.{agent uuid} using the signature from /broadcasting/auth.

EventPayloadMeaning
print-job.created{ job: PrintJobPayload }Fetch and print. Sent by App\Events\PrintJobCreated.
print-agent.revoked{}The shop removed this computer; forget the pairing. Sent by App\Events\PrintAgentRevoked.

Both events implement ShouldBroadcastNow: they are fired from inside the queued render job or the delete action, and a second queue hop would only add latency.

WebSocket (server → browser) ​

On the existing private-shop.{id} channel, consumed with useBroadcasting().listenToShopChannel():

EventPayloadWho listens
PrintAgentsChanged{}The settings page reloads agents.
PrintJobUpdated{ id, status, error, document, printer }The settings page reloads recentJobs; the print menu toasts the outcome of jobs it sent.

Job lifecycle ​

pending ──render──▶ sent ──agent ack──▶ received ──▶ printed
   │                                        │
   └──render failed──▶ failed ◀─────────────┘ (print failed)
StatusSet byMeaning
pendingCreatePrintJobRow exists, PDF not rendered yet.
sentRenderPrintJobFile on disk, event broadcast, nobody has claimed it. Picked up on reconnect for printing.delivery_window_hours.
receivedagentDownloading and handing to the spooler.
printedagentHanded to the OS spooler. The file is deleted.
failedserver or agenterror holds the reason. The file is deleted.

printed means the operating system accepted the job; a paper jam after that is the printer's to report.

Browser API (browser → server) ​

RouteActionPurpose
GET shop.settings.printers.indexGetPrinterSettingsThe settings page.
GET shop.settings.printers.targetsGetPrintTargets{ agents, settings } for print menus, fetched on first open.
POST shop.settings.printers.agents.storeRegisterPrintAgentPair; answers the token once. Manager or owner.
PUT shop.settings.printers.agents.updateRenamePrintAgentRename.
DELETE shop.settings.printers.agents.destroyDeletePrintAgentUnpair, clear defaults, broadcast print-agent.revoked.
PUT shop.settings.printers.defaults.updateUpdatePrintSettings{ defaults: { estimate, work_order, invoice: { print_agent_id, printer, copies } | null } }. Validates the agent belongs to the shop and reports that printer.
POST shop.settings.printers.testPrintTestPageSends resources/views/print/test-page.blade.php to a printer.
POST shop.workOrders.printPrintWorkOrder{ print_agent_id?, printer?, copies? }; without a printer, the default for the document type applies, or 422 on printer explains what to configure.

Outside Inertia pages, enums in JSON responses are plain strings (status: "sent"), not the { __enum, value } envelope. PrintMenu.vue reads them as such.

Configuration ​

config/printing.php:

KeyEnvDefaultPurpose
diskPRINTING_DISKlocalPrivate disk holding rendered PDFs until printed.
pathprint-jobsDirectory on that disk.
online_threshold_minutes10An agent is online when its last heartbeat is younger than this.
delivery_window_hours24How long a sent job waits for an offline agent.
local_agent_urlPRINTING_LOCAL_AGENT_URLhttp://127.0.0.1:41780Where the browser looks for the agent.
websocket.host/port/schemePRINTING_WEBSOCKET_*broadcasting connectionReverb endpoint as seen from the shop.

Agent config (~/.vroum-connector/config.json): port, host, pairing { serverUrl, token, agentId }, allowedOrigins.