Docs menu

Authentication and conventions

Everything on this page is true of every endpoint: how you authenticate, what a request and a response look like, how lists page, and what an error actually tells you. Reading it once is what keeps the resource pages short.

Two things here are worth knowing before you write any code. Test mode gives you a virtual device and printer that runs the full job lifecycle, so an integration can be built and verified end to end before any hardware exists. And every response carries a Request-Id, which is the one value support needs to find your call in the logs.

Client libraries

The API is plain HTTP, so any language works. There are official clients for Node.js (printsocket on npm), PHP (printsocket/printsocket on Packagist), and Python (printsocket on PyPI). All of them cover every endpoint on these pages and handle the conventions below for you: retries with idempotency keys, cursor pagination, typed errors, and webhook signature verification. The source lives in the print-socket organization on GitHub: printsocket-node, printsocket-php, and printsocket-python.

npm install printsocket
composer require printsocket/printsocket
pip install printsocket

The same job, created from each:

import PrintSocket from "printsocket";

const ps = new PrintSocket({ apiKey: process.env.PRINTSOCKET_API_KEY });

const job = await ps.jobs.create({
  printer_id: "prn_8f2k1",
  title: "Order #12345 label",
  content: { format: "pdf", url: "https://example.com/label.pdf" },
});
use PrintSocket\PrintSocketClient;

$ps = new PrintSocketClient(['api_key' => getenv('PRINTSOCKET_API_KEY')]);

$job = $ps->jobs->create([
    'printer_id' => 'prn_8f2k1',
    'title' => 'Order #12345 label',
    'content' => ['format' => 'pdf', 'url' => 'https://example.com/label.pdf'],
]);
import os
import printsocket

ps = printsocket.PrintSocket(api_key=os.environ["PRINTSOCKET_API_KEY"])

job = ps.jobs.create({
    "printer_id": "prn_8f2k1",
    "title": "Order #12345 label",
    "content": {"format": "pdf", "url": "https://example.com/label.pdf"},
})
curl https://api.printsocket.com/v1/jobs \
  -H "Authorization: Bearer $PRINTSOCKET_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-12345" \
  -d '{
    "printer_id": "prn_8f2k1",
    "title": "Order #12345 label",
    "content": { "format": "pdf", "url": "https://example.com/label.pdf" }
  }'

From any other language, use the conventions on this page directly; nothing in the API requires a client library.

Machine-readable spec

Every endpoint on these pages is described by an OpenAPI 3.1 document:

  • https://www.printsocket.com/openapi.json
  • https://api.printsocket.com/v1/openapi.json (the same bytes, beside the API)

Point a code generator at it to get a client in a language we do not publish one for, import it into Postman or Insomnia to get a request collection, or read it to see the exact shape of every request and response, including which fields can be null.

The document covers the customer-facing API only. The endpoints under /agent are the protocol between PrintSocket and its own agent, they are authenticated by a device token rather than an API key, and they are deliberately left out: they are free to change.

Keys and scopes

  • Authorization: Bearer <key> on every request except /ping.
  • Keys look like sk_live_... or sk_test_.... The prefix identifies the mode. Secrets are shown once, at creation.
  • Each key carries a scope: read (GET only), print (read + create/cancel jobs), or manage (everything, including key and webhook management).
  • A read or print key can also be confined to a list of printers with printer_ids. It then sees only those printers, the jobs on them, and the devices and scales on the machines that host them. Anything else answers 404 as if it did not exist, so the key cannot tell which other ids are real. Use this for a key you hand to one location, one customer, or an automated agent. A manage key cannot be restricted, because it could create an unrestricted key.
  • The API sends no CORS headers, so a browser will not let a web page call it directly. Call it from your own server, which is where the key has to live anyway: a key shipped in page code is visible to anyone who views the source.

Test mode

sk_test_ keys get a virtual device and printer that simulates the full job lifecycle: queued → sent → printing → succeeded. You can build and test an integration end to end before any hardware is enrolled. Jobs sent to the virtual printer never count toward plan limits, on any plan.

Test-mode data is completely separate from live data: a test-mode key cannot see, list, or print to anything in the live pool, or the reverse. That makes test mode the right place to enroll a real printer you are experimenting with, since it can never appear in the fleet your production code enumerates. Real hardware bills the same in either pool, though: a job to a real printer counts whichever mode it was created in, because the paper is real either way. Your first two real devices in the test pool are exempt from the device limit, so checking an integration against hardware costs you nothing.

Utility

MethodPathNotes
GET/pingUnauthenticated liveness check. { "ok": true }
GET/meIntrospect the calling key: account, key id, scopes, mode (live/test), and printer_ids (null when the key can use every printer)

Requests and responses

  • JSON only (application/json), snake_case fields, timestamps in RFC 3339 UTC (2026-07-29T14:02:11Z).
  • Durations are always explicit-unit fields (expire_after_seconds), never bare numbers.
  • Every response carries a Request-Id header, and every error echoes it in the body. Quote it in support requests.
  • IDs are opaque and prefixed by type: dev_ device, prn_ printer, job_ job, doc_ document, whk_ webhook, key_ API key, scl_ scale. Treat them as strings; don't parse them.

Pagination

All list endpoints are cursor-based, newest first by default.

Request: ?limit=50&cursor=<opaque>&order=asc|desc (limit default 25, max 100)

{
  "data": [],
  "has_more": true,
  "next_cursor": "eyJpZCI6..."
}

Pass next_cursor back as cursor to fetch the next page. Stop when has_more is false.

Errors

One shape everywhere:

{
  "error": {
    "type": "invalid_request",
    "code": "printer_offline",
    "message": "Printer prn_8f2k1 is offline; the job was not accepted because queue_if_offline=false.",
    "param": "printer_id",
    "request_id": "req_9x4..."
  }
}

Branch on type for broad handling and code for specific cases. message is human-readable and may change; param names the offending field on validation errors and is absent otherwise.

type is one of invalid_request, authentication, permission, not_found, conflict, rate_limited, billing, or server. The HTTP status says the same thing more coarsely: 400 and 422 (validation), 401 (auth), 402 (plan limit), 403 (scope), 404, 405, 409 (state conflict), 413 (payload over 50 MB), 429 (with Retry-After), 5xx.

Error codes

Every code the API returns, grouped by what to do about it. Validation failures carry param, so one handler for invalid_parameter can report the field without parsing the message.

CodeStatusTypeWhen
missing_authorization401authenticationNo Authorization header
invalid_authorization401authenticationThe header is not Bearer sk_...
invalid_api_key401authenticationThe key is unknown or has been revoked
insufficient_scope403permissionThe key's scope does not allow this call (a read key creating a job, for example)
live_mode_required403permissionA test-mode key tried to create or revoke a live-mode key
rate_limited429rate_limitedSustained excess over the account's rate; wait Retry-After seconds
plan_limit_reached402billingThe plan's monthly job cap on real printers is reached; the message names the plan and the reset date
device_limit_reached402billingEnrolling would exceed the plan's device cap
invalid_json400invalid_requestThe body is not a single JSON value
invalid_request_body400invalid_requestThe body could not be read
invalid_parameter400invalid_requestA field or query parameter is wrong; param names it
invalid_idempotency_key400invalid_requestIdempotency-Key is longer than 255 characters
idempotency_key_reused400invalid_requestThe key was already used with a different request body
payload_too_large413invalid_requestThe body, or inline job content, is over 50 MB; upload a document or pass a URL instead
method_not_allowed405invalid_requestThe path exists but not for this method
printer_offline422invalid_requestThe printer is offline and the job set queue_if_offline: false
source_too_large422invalid_requestThe document fetched from source_url is over 50 MB
source_fetch_failed422invalid_requestsource_url could not be fetched; the message says why
resource_not_found404not_foundNo such endpoint
device_not_found, printer_not_found, scale_not_found, job_not_found, document_not_found, webhook_not_found, api_key_not_found404not_foundNo such resource in this account and mode. A live key asking for a test-mode printer gets this, not a permission error
job_not_cancelable409conflictThe job is already printing or finished
printer_disabled409conflictThe printer's enabled is false, so it rejects new jobs
device_already_revoked409conflictA second revoke of the same device
device_virtual409conflictA revoke of the test-mode fixture device, which cannot be undone and so is refused
idempotency_key_in_flight409conflictThe original request with this key is still running; retry after it finishes
documents_unavailable503serverDocument storage is not configured on this server
internal_error500serverSomething on our side; the request_id is what to send us

Codes are stable. New ones can be added, so treat an unknown code as its type rather than failing on it.

Idempotency

Send Idempotency-Key: <string> on any POST. A replay of the same key within 24 h returns the stored original response (the same status and the same body), so a network retry can't create a duplicate job. There is no conflict to recover from; just retry with the same key.

Rate limiting

  • Standard headers on every response: RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset.
  • Sustained excess returns 429 with Retry-After. The default is 20 requests/second per account, burstable.

Metadata

Every mutable resource accepts "metadata": { "order_id": "12345", ... }, with up to 20 keys and string values of 500 characters or less. Attach your own identifiers, then filter by them on any list endpoint: ?metadata[order_id]=12345. This is the intended way to correlate PrintSocket resources with records in your own system.

Versioning

The major version is in the path (/v1). Additive changes (new fields, new endpoints, new enum values) are not breaking and can land at any time, so parse defensively. Breaking changes would ship as /v2.