Skip to content

Alerting

ZenPrint raises events when something happens in your org: a driver gets captured, a queue drifts from its config, a printer goes offline, toner runs low. You route those events to two kinds of channel.

  • Webhooks are for systems to react. Each one is a signed HTTP POST to a URL you control, with retries, so you can drive a PSA ticket, a chat message, or any automation.
  • Email is for people to be told. It sends a plain notification to a list of recipients. There is no payload, signature, or retry. It is the simplest way to get a human looking at a problem.

You configure all of this under Settings → Alerting, which needs the Webhooks permission (view to look, edit to change).

How the tab is laid out

The Alerting tab has three parts, top to bottom.

  1. Supply thresholds. The supply level that triggers a low alert, set once per consumable (toner, ink, drum, and so on). Blank uses the default of 15%. This is just the trigger; where the alert goes is set in the channel tables.
  2. Email channel. Your SMTP relay and recipients, then a table of events with two checkboxes each: Alert (email when the condition starts) and Resolved (also email when it clears).
  3. Webhooks. Your endpoints, then a table of events with one checkbox per endpoint plus a Resolved checkbox per event.

Email is off by default, so check the events you want emailed. New webhook endpoints start subscribed to every event, so untick the ones you don’t want.

Recovery is per channel. The Resolved checkbox in the email table and the one in the webhook table are independent, and both only apply to events whose clear ZenPrint detects: the supply events and printer.offline. So you can have a webhook auto-close a PSA ticket when a printer comes back, without emailing people the good news, or the other way round. A resolved notification is the same event flagged as resolved (the email subject reads “Resolved:”, the webhook payload carries "resolved": true).

Email channel

Email uses your own SMTP relay, so alerts come from an address you control.

  1. Under Settings → Alerting → Email channel, fill in your SMTP host, port, username, password, from address, and security (STARTTLS, implicit TLS, or none). Click Save SMTP. Leaving the password blank on a later save keeps the stored one.
  2. Add Recipients. Everyone on the list receives every event you tick in the email table.
  3. Click Send test to confirm the relay works. It emails your recipients a short test message.
  4. In the email table, tick Alert for the events you want emailed, and Resolved where you also want a recovery email.

Adding a webhook

  1. Under Settings → Alerting, in Webhook endpoints, click Add Webhook.
  2. Fill in the form:
    • Payload URL is the https:// endpoint that receives the POST. It must be a publicly reachable address. ZenPrint refuses to deliver to internal addresses (loopback, private, or link-local ranges), so a URL like http://localhost or http://192.168.x.x won’t work.
    • Description is optional, just a note to yourself. It also becomes the column header for this endpoint in the webhook table.
  3. Click Save. ZenPrint shows the signing secret once. Copy it now, because you can’t see it again. If you lose it, delete the webhook and make a new one.
  4. In the webhook table below the endpoint list, tick the events this endpoint should receive.

The Test button sends a one-off signed webhook.test event so you can check your receiver before relying on real traffic. Edit changes the URL or description. The Enabled toggle pauses delivery without throwing away the webhook or its history.

Events

  • capture.complete — a driver capture finishes and the driver is saved.
  • drift.detected — a reconcile report shows an installed queue whose config no longer matches its desired settings.
  • endpoint.unmapped — a scan reports an endpoint still unmapped to a client and location 24 hours after it was first seen.
  • printer.offline — a scan confirms a printer has gone missing: every endpoint that used to see it has since scanned without finding it.
  • supply.low.toner — a toner cartridge at or below 15%.
  • supply.low.ink — an ink cartridge at or below 15%.
  • supply.low.drum — an imaging drum at or below 15%.
  • supply.low.fuser — a fuser kit at or below 15%.
  • supply.low.transfer — a transfer kit at or below 15%.
  • supply.low.developer — a developer unit at or below 15%.
  • supply.low.waste — a waste container at or below 15% remaining capacity (i.e. nearly full).

Supply levels are reported as remaining capacity, so every supply (waste included) alerts on the same “at or below the threshold” rule. The threshold defaults to 15% and is set per event in the Supply thresholds strip at the top of the tab. The alerts are granular: subscribe per consumable, so you can be paged for toner but not for the fuser kit. Maintenance items the printer also meters (cleaning-roller counters and similar) never alert.

The threshold is shared by both channels (it’s the trigger, not a delivery setting), but the subscriptions and the recovery toggles are per channel. A toner-low event that crosses your threshold is sent to whichever channels you ticked for it, webhook or email or both.

A printer is only marked missing when an endpoint that previously saw it scans again and it’s gone. A slow or paused scan schedule reads as “stale”, not offline, so a quiet fleet won’t generate false alarms; a scan has to be more than about a week old before it stops counting as a recent confirmation.

Every event is edge-triggered: it fires once when a condition starts and stays quiet while the condition persists, even though the scripts run repeatedly. The condition has to clear and recur before you get another notification: a queue drifts, you fix it, it drifts again; an endpoint is unmapped, you map it, a different one goes unmapped; toner runs low, you replace it, it runs low again. So you won’t get the same alert on every scan or reconcile, and you don’t need to dedupe on your side. capture.complete is a one-off by nature (it fires per capture).

Payload format

Each delivery is a POST with a JSON body:

{
"event": "drift.detected",
"organization_id": "0b3f…",
"timestamp": "2026-06-02T12:00:00Z",
"data": {
"endpoint_id": "9c1a…",
"hostname": "LAPTOP-42",
"drifted_queues": [
{ "queue_id": "44de…", "queue_name": "Accounting-MFP" }
]
}
}

The data object differs per event. These headers are on every request:

HeaderValue
X-ZenPrint-Eventthe event name, e.g. drift.detected
X-ZenPrint-Deliveryunique delivery ID (absent on Test pings)
X-ZenPrint-Signaturesha256=<hex HMAC-SHA256 of the raw body, keyed with your signing secret>

Verifying the signature

Verification is optional but recommended. There’s no universal standard for webhook signatures, so a receiver can only check ours if you give it the scheme: the signature is HMAC-SHA256 over the raw request body, keyed with the signing secret shown when you created the webhook, and sent as sha256=<hex> in the X-ZenPrint-Signature header. Compute the same HMAC and compare in constant time; reject on mismatch.

If your receiver is a no-code or PSA integration that doesn’t verify signatures, keep the payload URL secret and rely on HTTPS. If you’re wiring up a custom receiver:

import hmac, hashlib
def valid(body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)

Delivery and retries

A background worker delivers each event. Your endpoint has 10 seconds to respond, so return a 2xx quickly and do any slow work asynchronously. If it returns a non-2xx status or the connection fails, ZenPrint retries with growing gaps: up to 6 attempts, starting 30 seconds apart and doubling each time (30s, 1m, 2m, 4m, 8m). After the last failed attempt the delivery is marked exhausted and isn’t retried again.