Python client for the PrintSocket cloud print API.
A lightweight agent runs on a machine, connects outbound to PrintSocket, and exposes that machine's printers (and scales) to a REST API. This library wraps API v1: devices, printers, scales, documents, print jobs, webhooks, and API keys, plus webhook signature verification for your receiver.
Zero dependencies; the standard library does all the work. Requires Python 3.10 or newer, and ships with inline type hints. Full API documentation lives at www.printsocket.com/docs.
pip install printsocketAn sk_test_ key comes with a virtual device and printer that runs the full
job lifecycle, so this works before any hardware is enrolled:
import os
import printsocket
ps = printsocket.PrintSocket(api_key=os.environ["PRINTSOCKET_API_KEY"])
printers = ps.printers.list({"state": "online"})
job = ps.jobs.create({
"printer_id": printers["data"][0]["id"],
"title": "Order #12345 label",
"content": {"format": "pdf", "url": "https://example.com/label.pdf"},
"metadata": {"order_id": "12345"},
})
print(job["id"], job["status"]) # job_... queuedResponses are dicts with the exact snake_case fields the API reference documents, so the docs read straight across to the code.
ps = printsocket.PrintSocket(
api_key="sk_live_...", # required
base_url="https://api.printsocket.com/v1", # default
timeout=30.0, # seconds per attempt
max_retries=2, # connection failures, 429s, and 5xx
)Every API error raises a typed subclass of APIError carrying status,
type, code, param, and request_id (quote the request id in support
requests):
import printsocket
try:
ps.jobs.cancel(job_id)
except printsocket.ConflictError as e:
if e.code == "job_not_cancelable":
... # already printing or finished
else:
raiseThe classes are InvalidRequestError, AuthenticationError,
PermissionDeniedError, NotFoundError, ConflictError, RateLimitError,
BillingError, and ServerError, one per error.type the API returns.
Requests that never got a response raise APIConnectionError.
Connection failures, 429s, and 5xx responses are retried automatically
(max_retries, default 2), honoring Retry-After. Every POST carries an
Idempotency-Key header, generated when you do not pass one, and the key is
identical across the client's own retry attempts, so a retried create cannot
produce a duplicate job. To extend the guarantee across your own retries,
pass a key derived from your record:
ps.jobs.create(params, idempotency_key="order-12345-label")list() returns one page (data, has_more, next_cursor). Each list
resource also has iterate(), which follows cursors for you:
for job in ps.jobs.iterate({"status": "failed", "limit": 100}):
print(job["id"], (job.get("error") or {}).get("message"))Upload once, print many times:
with open("packing-slip.pdf", "rb") as f:
doc = ps.documents.upload(f.read(), "application/pdf", expire_after_seconds=3600)
ps.jobs.create({
"printer_id": "prn_...",
"content": {"format": "pdf", "document_id": doc["id"]},
})ps.documents.create_from_url({"source_url": ...}) has the API fetch the
file server-side instead.
Subscribe with the client, verify deliveries with printsocket.webhook.
Verification needs the raw request body; a decoded and re-encoded body will
not match the signature.
import printsocket
from printsocket import webhook
endpoint = ps.webhooks.create({
"url": "https://example.com/printsocket/webhook",
"events": ["job.*", "printer.state_changed"],
})
# endpoint["secret"] is shown only this once; store it.
# In your receiver (Flask shown; any framework works the same way):
@app.post("/printsocket/webhook")
def receive():
try:
event = webhook.construct_event(
request.get_data(),
request.headers.get("PrintSocket-Signature", ""),
os.environ["PRINTSOCKET_WEBHOOK_SECRET"],
)
except printsocket.WebhookVerificationError:
return "", 400
# Delivery is at-least-once: dedupe on event["id"] before acting.
return "", 200Generate a short-lived, single-use token server-side and hand it to the agent installer, so your API keys never touch a customer machine:
token = ps.enrollment_tokens.create({"name": "Front desk PC"})
# token["token"] is the secret; it expires in about an hour.scale = ps.scales.get("scl_...")
reading = scale.get("reading")
if reading and reading["stable"]:
print(reading["weight_grams"], "g at", reading["captured_at"])PYTHONPATH=src python -m unittest discover -s testsThe test suite uses only the standard library on purpose; it runs anywhere Python does, with no install step.
MIT