API reference

HTTP interface

Using printlink.v1 over HTTP: POST /rpc shares one port and one message shape with the WebSocket, for mobile apps, native desktops and servers.

Last updated

printlink.v1 has two transports on the same port:

TransportEndpointSuits
WebSocketws://127.0.0.1:17888/wsWeb pages, with event push
HTTPhttp://127.0.0.1:17888/rpcMobile apps, native desktops, server jobs; stateless

The messages are identical: an HTTP request body is the WebSocket request frame. Methods, parameters, permissions, rate limits and error codes are all shared — there is no "HTTP-only interface".

Web pages should use the WebSocket

/rpc deliberately sends no CORS headers. A browser page calling it cross-origin is blocked by the browser, which is a design trade-off rather than a defect: pages should load /sdk/printlink.js and use the WebSocket, where origin checking, pairing and event push all exist. The HTTP transport targets clients that have no CORS semantics and cannot easily hold a connection open.

Routes

RouteNotes
POST /rpcOne RPC call
PUT /rpc/assets/{uploadId}Asset chunk data
GET /healthzLiveness probe, no auth
GET /api/v1/printers/{queueId}/statusPrinter status snapshot
GET /api/v1/printers/{queueId}/telemetryConsumables / counters
GET /api/v1/scans/{scanId}/pages/{index}Scan page download

GET /rpc returns 405. Every route sits behind the source allow-list, see security.

Messages

// request body
{ "type": "request", "v": 1, "id": "req_1", "method": "printer.list", "params": {} }

// success
{ "type": "response", "v": 1, "id": "req_1", "ok": true, "result": { "devices": [], "queues": [] } }

// failure
{ "type": "response", "v": 1, "id": "req_1", "ok": false,
  "error": { "code": "PERMISSION_DENIED", "message": "printer.read required", "retryable": false } }
  • type may be omitted (treated as request).
  • id may be omitted; it defaults to "" and is echoed back.
  • v must be 1, otherwise PROTOCOL_MISMATCH.

Authentication

HTTP has no connection, so the session is rebuilt from each request's headers. That makes token revocation take effect immediately.

CallerHeadersResult
Local native clientNo Origin, from the loopbackTrusted local session, no token needed
Remote no-Origin clientNo Origin, from the networkRefused: REMOTE_NATIVE_CLIENT_DENIED
App holding a tokenAuthorization: Bearer plk_…Authorised session; permissions = the token's
App without a tokenOrigin onlyOnly handshake and authorisation methods

HTTP always requires a token. The source address is not an HTTP credential — each request stands alone, so treating the address as a credential would give any machine on the subnet the same reach as a program the user approved.

Since v1.0.10 the user issues tokens in the console

auth.pair no longer returns a token. Over HTTP it fails with PAIRING_REQUIRED and details.remedy = "issue_token", telling you to create one in PrintLink Console → Security center → Tokens.

Pass the token to your program as configuration (an environment variable or a config file) instead of asking for one at runtime:

curl -X POST http://127.0.0.1:17888/rpc \
  -H "Authorization: Bearer plk_…" \
  -H "Content-Type: application/json" \
  -d '{"v":1,"type":"request","id":"1","method":"printer.list","params":{}}'

Store the token safely and never write it to a log.

The HTTP access switch

Console → Security center → Tokens has an HTTP access switch, on by default. When it is off, HTTP carries no protected call at all (the static routes /sdk/printlink.js / /healthz / / and the authorisation methods are unaffected), and WebSocket is untouched.

Uploading assets

The WebSocket carries chunk metadata in binary frames; HTTP moves it to the URL and headers:

FieldHTTP equivalent
uploadIdPath /rpc/assets/{uploadId}
offsetHeader X-PrintLink-Offset (required; must equal the bytes received so far)
finalHeader X-PrintLink-Final (1 / true / yes; defaults to true)
payloadThe raw request body

A successful chunk returns a transport-level acknowledgement, not the full envelope:

{ "uploadId": "upl_…", "offset": 2726 }

The full flow: asset.begin → one or more PUTs (non-final chunks carry X-PrintLink-Final: false) → asset.commit → an AssetRef. When asset.begin hits the content-addressed cache it returns the asset directly, so the chunks can be skipped entirely.

Status codes are only a convenience

The response body is always the complete envelope; ok / error.code is authoritative. Branch on the code, not the status:

StatusCorresponding codes
200Success
400Other client errors (INVALID_REQUEST, DOCUMENT_INVALID, PROTOCOL_MISMATCH…)
401TOKEN_INVALID, PAIRING_REQUIRED
403PERMISSION_DENIED, ORIGIN_NOT_ALLOWED, PAIRING_DENIED, PAIRING_PENDING, REMOTE_NATIVE_CLIENT_DENIED…
404PRINTER_NOT_FOUND, PROFILE_NOT_FOUND, JOB_NOT_FOUND, METHOD_NOT_FOUND…
413PAYLOAD_TOO_LARGE
429RATE_LIMITED
503QUEUE_FULL
500INTERNAL

No event push

HTTP is plain request/response. Poll job.get / job.list for progress, and bound the polling; the terminal states are only completed / failed / canceled. When you need live push, go back to the WebSocket — both transports coexist on one port and one app may mix them freely.

A complete example

# 1) List printers (the token comes from Console → Security center → Tokens;
#    its plaintext is shown exactly once)
curl -s -X POST http://127.0.0.1:17888/rpc \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer plk_…' \
  -d '{"v":1,"id":"2","method":"printer.list","params":{}}'

# 2) List printers
curl -s -X POST http://127.0.0.1:17888/rpc \
  -H 'Origin: http://com.example.printapp' -H 'Authorization: Bearer plk_…' \
  -d '{"v":1,"id":"2","method":"printer.list","params":{}}'

# 3) Upload a PDF
curl -s -X POST http://127.0.0.1:17888/rpc \
  -H 'Origin: http://com.example.printapp' -H 'Authorization: Bearer plk_…' \
  -d '{"v":1,"id":"3","method":"asset.begin",
       "params":{"mime":"application/pdf","size":2726,"sha256":"<sha256>"}}'
curl -s -X PUT http://127.0.0.1:17888/rpc/assets/<uploadId> \
  -H 'Origin: http://com.example.printapp' -H 'Authorization: Bearer plk_…' \
  -H 'X-PrintLink-Offset: 0' -H 'X-PrintLink-Final: true' \
  --data-binary @file.pdf
curl -s -X POST http://127.0.0.1:17888/rpc \
  -H 'Origin: http://com.example.printapp' -H 'Authorization: Bearer plk_…' \
  -d '{"v":1,"id":"4","method":"asset.commit","params":{"uploadId":"<uploadId>","sha256":"<sha256>"}}'

# 4) Print, then poll for the terminal state
curl -s -X POST http://127.0.0.1:17888/rpc \
  -H 'Origin: http://com.example.printapp' -H 'Authorization: Bearer plk_…' \
  -d '{"v":1,"id":"5","method":"job.print","params":{"queueId":"prq_…","title":"demo",
       "source":{"items":[{"type":"pdf","asset":{"id":"asl_…","sha256":"<sha256>","mime":"application/pdf","size":2726}}]}}}'
curl -s -X POST http://127.0.0.1:17888/rpc \
  -H 'Origin: http://com.example.printapp' -H 'Authorization: Bearer plk_…' \
  -d '{"v":1,"id":"6","method":"job.get","params":{"jobId":"job_…"}}'

The repository's examples/http-print/print.mjs is a zero-dependency Node client for the same flow:

node examples/http-print/print.mjs            # validate only, no paper
node examples/http-print/print.mjs --print    # a real silent print
node examples/http-print/print.mjs --local    # local native: no Origin, no pairing

To watch each request in a browser, use the Playground's HTTP page (http://127.0.0.1:17888/playground/http/).

Differences from the WebSocket

AspectWebSocketHTTP
SessionKept for the connectionRebuilt per request (revocation is instant)
EventsPushedNone; poll instead
Asset chunksBinary framesPUT /rpc/assets/{uploadId}
CORSN/ANot sent (pages should use the WebSocket)
Methods / permissions / error codes—Identical
Capability resolutionMandatoryEqually mandatory

See also

On this page