Webhooks API
Get push notifications when a print job finishes or fails, instead of polling job status. Endpoint management, event payloads, signature verification, and retries.
Webhooks API
Polling GET /v1/print-jobs/{id} tells you what happened, eventually. Webhooks
tell you as it happens: PrintBase POSTs a signed JSON event to your URL the
moment a job reaches a terminal state.
Webhooks are available on paid plans. Creating or updating an endpoint on the
free plan returns 403 PLAN_LIMIT_EXCEEDED.
Events
| Event | Fires when |
|---|---|
print_job.completed | The agent reported the job printed successfully |
print_job.failed | The job reached a terminal failure — see reason_code |
An endpoint only receives the events listed in its events array. Unknown
event names are silently dropped when the endpoint is saved, so check the
response body to confirm what was stored.
Create an endpoint
POST /v1/webhooks — requires the api_keys:write scope.
curl https://api.printbase.cloud/v1/webhooks \
-H "Authorization: Bearer pb_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Fulfillment service",
"url": "https://example.com/hooks/printbase",
"events": ["print_job.completed", "print_job.failed"]
}'Response:
{
"id": "wh_abc",
"organization_id": "org_abc",
"name": "Fulfillment service",
"url": "https://example.com/hooks/printbase",
"secret": "whsec_...",
"events": ["print_job.completed", "print_job.failed"],
"status": "active",
"created_at": "2026-08-22T09:11:04Z",
"updated_at": "2026-08-22T09:11:04Z"
}Fields:
nameandurlare required. The URL must be a public HTTP/HTTPS address — private and loopback addresses are rejected withINVALID_REQUESTso an endpoint cannot be pointed at internal infrastructure.secretis optional. Omit it and PrintBase generates awhsec_...value; it is returned in this response and inGET, so you can also read it later.statusisactiveordisabled, defaulting toactive. A disabled endpoint receives nothing and cannot be tested.
Manage endpoints
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /v1/webhooks | api_keys:read | List endpoints |
GET | /v1/webhooks/{id} | api_keys:read | Read one endpoint, including its secret |
PATCH | /v1/webhooks/{id} | api_keys:write | Update name, url, secret, events, status |
DELETE | /v1/webhooks/{id} | api_keys:write | Delete the endpoint |
POST | /v1/webhooks/{id}/test | api_keys:write | Send a sample print_job.completed event |
GET | /v1/webhooks/{id}/deliveries | api_keys:read | Recent delivery attempts |
PATCH applies only the fields you send; omitted fields keep their current
value. Sending events replaces the whole list.
Event payload
Every delivery is a POST with a JSON body of the same shape:
{
"id": "evt_abc",
"type": "print_job.failed",
"created_at": "2026-08-22T09:12:41Z",
"organization_id": "org_abc",
"data": {
"job_id": "job_abc",
"printer_id": "printer_123",
"printer_name": "Warehouse Zebra",
"computer_id": "cmp_456",
"computer_name": "wh-pc-02",
"status": "failed",
"reason_code": "PRINTER_OFFLINE",
"message": "printer is offline",
"created_at": "2026-08-22T09:12:30Z",
"last_updated_at": "2026-08-22T09:12:41Z"
}
}data.status is completed or failed. On failure, data.reason_code is one
of AGENT_OFFLINE, PRINTER_OFFLINE, DOWNLOAD_FAILED, PRINT_ERROR, or
INVALID_CONTENT — see the Print Jobs API for what
each one means.
Headers on every request:
| Header | Value |
|---|---|
X-PrintBase-Event | The event type, e.g. print_job.completed |
X-PrintBase-Event-Id | The event id, e.g. evt_abc — use it to deduplicate |
X-PrintBase-Signature | t=<unix-seconds>,v1=<hex hmac> |
Verify the signature
The signature is an HMAC-SHA256 over "{timestamp}.{raw body}", keyed with the
endpoint secret. Verify against the raw request body — parsing and
re-serializing the JSON changes the bytes and breaks the comparison.
import crypto from 'node:crypto';
export function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.split('=').map((s) => s.trim()))
);
const { t, v1 } = parts;
if (!t || !v1) return false;
// reject replays of an old, valid signature
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(v1, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}In Express, keep the raw body around:
app.post(
'/hooks/printbase',
express.raw({ type: 'application/json' }),
(req, res) => {
const raw = req.body.toString('utf8');
if (!verify(raw, req.get('X-PrintBase-Signature'), process.env.PRINTBASE_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
res.sendStatus(200); // acknowledge first
handleEvent(JSON.parse(raw)); // then do the slow work
}
);Retries and delivery records
A delivery counts as successful on any 2xx response. Anything else — a
non-2xx status, a timeout, a connection error — is retried twice, after
5 seconds and then 30 seconds. After the third failed attempt the delivery is
marked failed and is not retried again. Requests time out after 5 seconds, so
acknowledge quickly and process asynchronously.
Because a retry can follow a response your handler already processed (a slow
200 that arrived after the timeout, for example), handlers must be
idempotent. Deduplicate on X-PrintBase-Event-Id.
Inspect what happened with the deliveries endpoint:
curl "https://api.printbase.cloud/v1/webhooks/wh_abc/deliveries?limit=20" \
-H "Authorization: Bearer pb_live_xxx"[
{
"id": "whd_abc",
"endpoint_id": "wh_abc",
"event_id": "evt_abc",
"event_type": "print_job.failed",
"job_id": "job_abc",
"status": "retrying",
"attempts": 1,
"last_http_status": 502,
"last_error": "non-2xx response",
"next_retry_at": "2026-08-22T09:12:46Z",
"created_at": "2026-08-22T09:12:41Z",
"updated_at": "2026-08-22T09:12:41Z"
}
]status is pending, retrying, success, or failed. limit defaults to
50 and accepts up to 200.
Test an endpoint
POST /v1/webhooks/{id}/test sends a sample print_job.completed event with
job_id: "job_test", signed exactly like a real one — useful for checking your
verification code before printing anything.
curl -X POST https://api.printbase.cloud/v1/webhooks/wh_abc/test \
-H "Authorization: Bearer pb_live_xxx"{ "status": "queued", "delivery_id": "whd_xyz" }The response returns as soon as the delivery is queued; check
/v1/webhooks/{id}/deliveries for the outcome.