Docs menu

Webhooks and API keys

A job moves through several states, and there are two ways to find out about it: poll GET /jobs/{id}/events, or subscribe and have the transitions pushed to you. Polling is fine while you are building. Webhooks are what you want in production, because a job can sit queued until the device comes back online or the job expires, and there is nothing useful to poll in the meantime.

Delivery is at-least-once and every payload is signed, so a receiver has two jobs beyond reading the event: verify the signature, and dedupe on the event id. Both are covered below, with a receiver you can paste.

API keys are on this page because they are the other thing you manage rather than simply use. Keys are how your own server authenticates. Enrollment tokens, on the devices page, are how a customer's machine does.

Webhooks

MethodPathNotes
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}/testSends a synthetic ping event so you can verify your receiver

Event types, subscribable with wildcards (job.*, or * for everything):

  • job.created, job.queued, job.sent, job.printing, job.succeeded, job.failed, job.canceled, job.expired
  • printer.state_changed, printer.discovered, printer.removed
  • device.state_changed, device.enrolled, device.revoked
  • webhook.disabled, sent to your other endpoints when one of them is switched off for failing

Delivery payload:

{
  "id": "evt_3mm2z",
  "type": "job.succeeded",
  "created_at": "2026-07-29T14:02:40Z",
  "data": { "id": "job_5tq8p", "object": "job", "status": "succeeded" }
}

data is the full object the event is about: the whole job for a job.* event, the printer for printer.*, and so on. It is the same shape GET returns, so a receiver can act on the payload without a second request.

  • Every delivery is signed: PrintSocket-Signature: t=<unix>,v1=<hmac-sha256> computed over t.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.disabled notification event.
  • Delivery is at-least-once, so dedupe on the evt_ id.

Receiving a webhook

Three rules make a receiver correct. Verify the signature against the raw request body, before parsing it: a body that has been decoded and re-encoded will not match. Respond 2xx within ten seconds and do the work afterwards, because a slow response counts as a failure and is retried. And record each evt_ id you have handled, since a retry after a timeout delivers the same event twice.

Each client library ships the verification, so a receiver is a few lines:

import express from "express";
import { webhooks, WebhookVerificationError } from "printsocket";

const app = express();
app.post("/printsocket/webhook", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = webhooks.constructEvent(
      req.body,
      req.headers["printsocket-signature"],
      process.env.PRINTSOCKET_WEBHOOK_SECRET,
    );
  } catch (err) {
    if (err instanceof WebhookVerificationError) return res.sendStatus(400);
    throw err;
  }
  // Delivery is at-least-once: dedupe on event.id before acting.
  console.log(event.type, event.data.id);
  res.sendStatus(200);
});
use PrintSocket\Webhook;
use PrintSocket\Exception\WebhookVerificationException;

try {
    $event = Webhook::constructEvent(
        file_get_contents('php://input'),
        $_SERVER['HTTP_PRINTSOCKET_SIGNATURE'] ?? '',
        getenv('PRINTSOCKET_WEBHOOK_SECRET'),
    );
} catch (WebhookVerificationException $e) {
    http_response_code(400);
    exit;
}
// Delivery is at-least-once: dedupe on $event['id'] before acting.
http_response_code(200);
import os
import printsocket
from printsocket import webhook

@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 "", 200
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody is the request body as bytes, exactly as received.
function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
  if (!parts.t || !parts.v1 || age > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.`)
    .update(rawBody)
    .digest("hex");
  const a = Buffer.from(parts.v1);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

Without a library, the steps are: split the header on commas into t and v1, reject if t is more than 300 seconds from now, compute HMAC-SHA256 with the endpoint's secret over the timestamp, a period, and the raw body, and compare the hex digest to v1 in constant time. POST /webhooks/{id}/test sends a signed ping event, which is the quickest way to check a receiver before any job runs.

API keys

Requires the manage scope.

MethodPathNotes
GET/api_keysSecrets are never re-shown; returns the prefix and last 4 characters, plus last_used_at (null if the key has never been used, otherwise accurate to about a minute)
POST/api_keys{ "name": "...", "scopes": ["print"], "mode": "live" } → secret shown once. Add "printer_ids": ["prn_..."] (1 to 50 printers in the key's mode) to confine the key to those printers; omit it for every printer
DELETE/api_keys/{id}Immediate revocation

Account and billing management lives in the dashboard rather than this API.