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:
| Transport | Endpoint | Suits |
|---|---|---|
| WebSocket | ws://127.0.0.1:17888/ws | Web pages, with event push |
| HTTP | http://127.0.0.1:17888/rpc | Mobile 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
| Route | Notes |
|---|---|
POST /rpc | One RPC call |
PUT /rpc/assets/{uploadId} | Asset chunk data |
GET /healthz | Liveness probe, no auth |
GET /api/v1/printers/{queueId}/status | Printer status snapshot |
GET /api/v1/printers/{queueId}/telemetry | Consumables / 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 } }typemay be omitted (treated asrequest).idmay be omitted; it defaults to""and is echoed back.vmust be1, otherwisePROTOCOL_MISMATCH.
Authentication
HTTP has no connection, so the session is rebuilt from each request's headers. That makes token revocation take effect immediately.
| Caller | Headers | Result |
|---|---|---|
| Local native client | No Origin, from the loopback | Trusted local session, no token needed |
Remote no-Origin client | No Origin, from the network | Refused: REMOTE_NATIVE_CLIENT_DENIED |
| App holding a token | Authorization: Bearer plk_… | Authorised session; permissions = the token's |
| App without a token | Origin only | Only 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:
| Field | HTTP equivalent |
|---|---|
uploadId | Path /rpc/assets/{uploadId} |
offset | Header X-PrintLink-Offset (required; must equal the bytes received so far) |
final | Header X-PrintLink-Final (1 / true / yes; defaults to true) |
| payload | The 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:
| Status | Corresponding codes |
|---|---|
| 200 | Success |
| 400 | Other client errors (INVALID_REQUEST, DOCUMENT_INVALID, PROTOCOL_MISMATCH…) |
| 401 | TOKEN_INVALID, PAIRING_REQUIRED |
| 403 | PERMISSION_DENIED, ORIGIN_NOT_ALLOWED, PAIRING_DENIED, PAIRING_PENDING, REMOTE_NATIVE_CLIENT_DENIED… |
| 404 | PRINTER_NOT_FOUND, PROFILE_NOT_FOUND, JOB_NOT_FOUND, METHOD_NOT_FOUND… |
| 413 | PAYLOAD_TOO_LARGE |
| 429 | RATE_LIMITED |
| 503 | QUEUE_FULL |
| 500 | INTERNAL |
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 pairingTo watch each request in a browser, use the Playground's HTTP page
(http://127.0.0.1:17888/playground/http/).
Differences from the WebSocket
| Aspect | WebSocket | HTTP |
|---|---|---|
| Session | Kept for the connection | Rebuilt per request (revocation is instant) |
| Events | Pushed | None; poll instead |
| Asset chunks | Binary frames | PUT /rpc/assets/{uploadId} |
| CORS | N/A | Not sent (pages should use the WebSocket) |
| Methods / permissions / error codes | — | Identical |
| Capability resolution | Mandatory | Equally mandatory |