Docs
A quickstart, then the full v1 API reference — every endpoint, object, and error the API actually ships today.
Quickstart
1. Create an account
Sign up at https://app.printsocket.com. There is no password — you get a sign-in link by email. Name your account and you land on the dashboard.
2. Get an API key
In Settings → API keys, create a key. Pick test mode to start: test keys get a virtual device and printer that simulates the whole job lifecycle, so you can build before any hardware is involved. The secret is shown once — store it now.
curl https://api.printsocket.com/v1/me \
-H "Authorization: Bearer sk_test_..."
3. Enroll a machine
When you are ready for real printers, open Devices → Add device. That gives you a single-use enrollment token and the installer links. Install the agent on the machine that talks to your printers, then enroll it:
psagent enroll -token enr_... -server https://api.printsocket.com
psagent run
The agent connects outbound over a WebSocket — nothing needs to be reachable from the internet. It reports every printer queue it can see, with capabilities, and keeps them updated.
4. Find your printer
curl "https://api.printsocket.com/v1/printers?state=online" \
-H "Authorization: Bearer sk_test_..."
5. Send a job
curl https://api.printsocket.com/v1/jobs \
-H "Authorization: Bearer sk_test_..." \
-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" },
"options": { "paper": "A4" }
}'
Poll GET /v1/jobs/{id}, read the transition history at
GET /v1/jobs/{id}/events, or register a webhook and get pushed the state changes as
they happen. Everything on this page is also doable from the dashboard, including sending a test
print without writing any code.
API reference
PrintSocket API — v1 reference
A lightweight agent runs on your machine, connects outbound to PrintSocket, and exposes that machine's printers (and scales) to this REST API. No inbound ports, no print server to run.
Base URL: https://api.printsocket.com/v1
1. Authentication
Authorization: Bearer <key>on every request except/ping.- Keys look like
sk_live_...orsk_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), ormanage(everything, including key and webhook management).
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. Test-mode data is completely separate from live data, and test jobs never count toward plan limits.
2. Conventions
Requests and responses
- JSON only (
application/json),snake_casefields, 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-Idheader, 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 | authentication | permission | not_found | conflict | rate_limited | billing | server",
"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.
Status codes: 200/201, 400 (validation), 401 (auth), 402 (plan limit, type billing), 403 (scope), 404, 409 (state conflict), 413 (payload over 50 MB), 422 (semantically invalid, e.g. an option the printer doesn't support), 429 (with Retry-After), 5xx.
Plan limits return 402 with type billing, in live mode only: creating a job past your plan's monthly job cap gives plan_limit_reached (the message names the plan and the reset date), and enrolling past the device cap gives device_limit_reached.
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", ... } — up to 20 keys, 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.
3. Resources
3.1 Utility
| Method | Path | Notes |
|---|---|---|
| GET | /ping | Unauthenticated liveness check. { "ok": true } |
| GET | /me | Introspect the calling key: account, key id, scopes, and mode (live/test) |
3.2 Devices
A device is a machine running the PrintSocket agent.
| Method | Path | Notes |
|---|---|---|
| GET | /devices | Filters: state=online|offline, metadata[...] |
| GET | /devices/{id} | |
| PATCH | /devices/{id} | name, metadata |
| POST | /devices/{id}/revoke | Invalidates the agent's credentials. The device goes to state: revoked and cannot reconnect until it is enrolled again — reconnecting the machine will not bring it back. |
| POST | /enrollment_tokens | Returns a short-lived (1 h), single-use token for the agent installer. Body: optional name and metadata, pre-applied to the device when it enrolls. |
Device object:
{
"id": "dev_7hk2m",
"object": "device",
"name": "Front desk PC",
"state": "online", // online | offline | revoked
"hostname": "FRONTDESK-01",
"os": { "family": "windows", "version": "11" },
"agent_version": "1.4.2",
"last_seen_at": "2026-07-29T14:01:55Z",
"created_at": "...",
"metadata": {}
}
agent_version is the build the machine is running right now, not the one it enrolled with — it updates when the agent updates. os is recorded at enrollment time only.
3.3 Printers
Printers are discovered automatically by the agent. They are not created through the API.
| Method | Path | Notes |
|---|---|---|
| GET | /printers | Filters: device_id, state=online|offline, enabled=true|false, metadata[...] |
| GET | /printers/{id} | Includes full capabilities |
| PATCH | /printers/{id} | name (display alias), enabled (soft-disable: rejects new jobs), default_options, metadata |
Printer object:
{
"id": "prn_8f2k1",
"object": "printer",
"device_id": "dev_7hk2m",
"name": "Label printer", // your alias, PATCHable
"system_name": "ZDesigner ZD420", // as reported by the OS
"state": "online", // online | offline (offline whenever the device is offline)
"enabled": true,
"default_options": { "copies": 1 },
"capabilities": {
"color": false,
"duplex": false,
"copies_max": 99,
"dpis": ["203x203"],
"papers": { "4x6": [1016, 1524] }, // tenths of a mm
"trays": ["Main"],
"media": [],
"collate": true,
"custom_paper_size": false
},
"created_at": "...",
"metadata": {}
}
default_options merge underneath per-job options, so you can configure a printer once (always 203 dpi, tray 2) instead of repeating it on every job.
Removal. Agents report the complete queue list, so a printer missing from a report has been deleted from the OS — queues that are present but unreachable are still reported, as offline. A removed printer leaves every listing, GET /printers/{id} returns 404, and printer.removed fires. If the queue reappears under the same system_name, the printer revives with the same id and printer.discovered fires again.
3.4 Documents
Jobs can carry their content inline, but a document can be uploaded once and printed many times — one packing-slip PDF sent to three printers, for example, instead of re-sending or re-hosting the same bytes.
| Method | Path | Notes |
|---|---|---|
| POST | /documents | Either a direct upload (content_type: application/pdf, body up to 50 MB) or { "source_url": "...", "fetch_auth": {...} } for a server-side fetch |
| GET | /documents/{id} | |
| DELETE | /documents/{id} |
Documents expire after 24 h by default; override with expire_after_seconds.
3.5 Jobs
| Method | Path | Notes |
|---|---|---|
| POST | /jobs | Create (see below). Returns 201 with the full job object. |
| GET | /jobs | Filters: printer_id, device_id, status, created_after/created_before, metadata[...] |
| GET | /jobs/{id} | |
| POST | /jobs/{id}/cancel | Cancels if the job is not yet in a terminal state. Returns 409 job_not_cancelable if it is already printing or finished. |
| GET | /jobs/{id}/events | Ordered state-transition timeline for the job |
Create request (send Idempotency-Key as a header, not a body field):
{
"printer_id": "prn_8f2k1",
"title": "Order #12345 label",
"content": {
"format": "pdf", // pdf | raw (ZPL/ESC-POS/etc.)
"url": "https://...", // exactly one of url | base64 | document_id
"base64": "...",
"document_id": "doc_2ka9x",
"fetch_auth": { "type": "basic", "username": "...", "password": "..." } // only with url
},
"copies": 2,
"options": {
"duplex": "long_edge", // long_edge | short_edge | none
"color": false,
"paper": "A4",
"dpi": "300x300",
"page_ranges": "1,3-5",
"tray": "Tray 2",
"media": null,
"collate": true,
"fit_to_page": true,
"rotate": 0,
"pages_per_sheet": 1
},
"queue_if_offline": true, // false → rejected immediately with 422 printer_offline
"expire_after_seconds": 3600, // default 86400 (24 h)
"metadata": { "order_id": "12345" }
}
Job object:
{
"id": "job_5tq8p",
"object": "job",
"printer_id": "prn_8f2k1",
"device_id": "dev_7hk2m",
"title": "Order #12345 label",
"status": "printing",
"copies": 2,
"options": { ... },
"completion_confidence": null, // set on success: "printer" | "spooler" (see below)
"warnings": [ // non-fatal notices; the job still proceeds
{ "code": "option_unsupported", "message": "duplex printing requested but the printer does not report duplex support." }
],
"error": null, // { "code": "printer_error", "message": "Out of paper" } when failed
"created_at": "...",
"queued_at": "...",
"sent_at": "...",
"completed_at": null,
"expires_at": "...",
"metadata": { "order_id": "12345" }
}
Status lifecycle (a single status field, with strict transitions):
created ──► queued ──► sent ──► printing ──► succeeded
│ │ │
├──────────┴──────────┴──────► failed (terminal, error populated; straight from
│ queued when the agent can't start the job)
├────────────────────────────► canceled (terminal, via /cancel)
└────────────────────────────► expired (terminal, expire_after elapsed)
queued— accepted, waiting for the device to come online and pick it upsent— delivered to the agent and handed to the OS spoolerprinting/succeeded— reported wherever the OS driver supports it
Some drivers cannot confirm that a page actually came out. Those jobs go sent → succeeded when the spooler finishes, and the job carries "completion_confidence": "spooler" instead of "printer". If a print must be confirmed at the hardware, check that field rather than status alone.
Copies
copies is a single top-level field. PrintSocket hands it to the platform's own copy mechanism — -n on CUPS, DEVMODE on Windows, repeating the payload for raw content — rather than queuing the job several times. One job in, one job in the print queue.
Warnings
Capability mismatches are never rejected. Requesting an option the printer doesn't report support for doesn't fail the job — capability reporting is best-effort and agents can't always know. The job is accepted with a warnings entry attached, and webhook payloads carry the warnings too, so you can surface them without blocking prints that would have worked. raw content is likewise passed through to the printer unvalidated.
| Warning code | Meaning |
|---|---|
option_unsupported | An option was requested that the printer's reported capabilities don't cover (e.g. color: true or a duplex mode on a printer reporting neither). The option is passed to the driver anyway. |
copies_exceeds_max | copies is greater than the printer's reported copies_max. Behavior beyond the maximum is driver-defined. |
3.6 Scales
Read-only telemetry from USB scales attached to a device.
| Method | Path | Notes |
|---|---|---|
| GET | /scales | Filter: device_id |
| GET | /scales/{id} | Latest reading embedded |
Scale object:
{
"id": "scl_9d3f2",
"object": "scale",
"device_id": "dev_7hk2m",
"system_name": "Mettler Toledo PS60",
"state": "online", // online | offline (offline whenever the device is offline)
"reading": { // null until the agent reports one
"weight_grams": 1234,
"stable": true,
"captured_at": "2026-07-29T14:02:08Z"
},
"created_at": "..."
}
Scales are discovered and reported by the agent, like printers. A report without a fresh reading keeps the previous one, so check captured_at to see how stale a weight is, and stable to know whether the scale had settled. Read the weight on demand at the moment you need it — typically when buying a label. Scales carry no metadata and emit no webhook events.
3.7 Webhooks
| Method | Path | Notes |
|---|---|---|
| POST | /webhooks | { "url": "...", "events": ["job.*", "printer.state_changed"], "description": "..." } → returns the endpoint including its secret (shown once) |
| GET | /webhooks and /webhooks/{id} | |
| PATCH | /webhooks/{id} | url, events, disabled |
| DELETE | /webhooks/{id} | |
| POST | /webhooks/{id}/test | Sends a synthetic ping event so you can verify your receiver |
Event types, subscribable with wildcards:
job.created,job.queued,job.sent,job.printing,job.succeeded,job.failed,job.canceled,job.expiredprinter.state_changed,printer.discovered,printer.removeddevice.state_changed,device.enrolled,device.revoked
Delivery payload:
{
"id": "evt_3mm2z",
"type": "job.succeeded",
"created_at": "...",
"data": { ...full job object... }
}
- Every delivery is signed:
PrintSocket-Signature: t=<unix>,v1=<hmac-sha256>computed overt.payload. Verify it, and reject timestamps outside a 5-minute tolerance. Secrets are per endpoint and rotatable. - Failed deliveries retry with exponential backoff for 24 h. An endpoint that keeps failing is disabled automatically, with a
webhook.disablednotification event. - Delivery is at-least-once — dedupe on the
evt_id.
3.8 API keys
Requires the manage scope.
| Method | Path | Notes |
|---|---|---|
| GET | /api_keys | Secrets are never re-shown; returns the prefix and last 4 characters |
| POST | /api_keys | { "name": "...", "scopes": ["print"], "mode": "live" } → secret shown once |
| DELETE | /api_keys/{id} | Immediate revocation |
Account and billing management lives in the dashboard rather than this API.
4. Enrolling a device
- From your server,
POST /enrollment_tokens→{ "token": "enr_...", "expires_at": ... }. - Run the agent installer on the target machine with that token (CLI flag or paste into the GUI).
- The agent exchanges the token for device-scoped credentials. The device appears as
dev_...anddevice.enrolledfires. - Printers are discovered automatically; a
printer.discoveredevent fires for each.
Enrollment tokens are short-lived and single-use, and the credentials the agent ends up holding are scoped to that one device. Your account API keys never need to be embedded in an installer or stored on an end customer's machine.