Webhooks
Paqet delivers monitoring events to an HTTPS endpoint you control, signed so you can verify they came from us.
Webhooks are an Enterprise feature. Create and manage them in Console → Webhooks — each webhook has a URL, a destination type (Generic, Slack, or Discord), and a set of event types it subscribes to. A generated signing secret is shown once at creation.
How delivery works
- When a subscribed event fires, Paqet sends an HTTP
POSTto your URL with a JSON body. - Any
2xxresponse counts as delivered. Anything else — or a timeout — is a failure and is retried. - Delivery is per-webhook: if three of your webhooks subscribe to the same event, each gets its own POST and its own retry schedule.
- Every attempt (request, response, status, duration) is recorded under Console → Webhooks → Deliveries.
Payload envelope
Generic webhooks receive a signed envelope. The event-specific data is nested under Data;
the envelope keys are PascalCase.
{
"Id": "5b6e1e3a-9c7d-4f2a-8b1e-2c3d4e5f6a7b",
"Type": "incident.created",
"CreatedAt": "2026-09-01T12:00:00+00:00",
"Data": {
"DomainId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"Host": "example.com",
"CheckType": "HTTP",
"Status": "Error",
"Title": "HTTP Error: 503 Service Unavailable",
"Summary": "The endpoint returned 503 for 3 consecutive checks."
}
}
| Field | Type | Description |
|---|---|---|
| Id | string (uuid) | Unique delivery ID. Matches the X-Paqet-Delivery header. Use it to de-duplicate retries. |
| Type | string | The event's wire name (see Events). |
| CreatedAt | string (ISO 8601) | When the event was raised, UTC. |
| Data | object | Event-specific payload — see Event payloads. |
Headers
| Header | Description |
|---|---|
| X-Paqet-Event | The event's wire name, e.g. incident.created. |
| X-Paqet-Delivery | The delivery ID (same value as Id in the body). Stable across retries of the same event. |
| X-Paqet-Signature | t=<unix seconds>,v1=<hex> — the signed timestamp and HMAC. See Verifying signatures. |
| Authorization | Present only if you set an auth token on the webhook: Bearer <your token>. |
Verifying signatures
Recompute the signature yourself and compare it to v1 — never trust the header without checking it.
X-Paqet-Signature: t=1756728000,v1=a29680b4c1f2...
Formula
v1 = HMAC_SHA256(secret, t + "." + rawRequestBody) // lowercase hex
Node.js
const crypto = require("crypto");
function verify(secret, rawBody, signatureHeader) {
const parts = Object.fromEntries(signatureHeader.split(",").map(p => p.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
// Reject old timestamps to blunt replay attacks (Paqet uses a 5-minute tolerance).
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
- Hash the raw request body, byte-for-byte — not a re-serialized copy.
- Use a constant-time comparison.
- Reject deliveries whose
tis more than 5 minutes from your clock.
Retries
A failed delivery is retried up to 5 times with a fixed backoff:
attempt 1 → immediately
attempt 2 → +30 seconds
attempt 3 → +2 minutes
attempt 4 → +10 minutes
attempt 5 → +30 minutes
attempt 6 → +2 hours
After that the delivery is abandoned. Make your handler idempotent on X-Paqet-Delivery —
a retry can arrive after your endpoint has, in fact, already processed the first attempt.
If your Enterprise plan lapses, pending retries stop and the webhook is disabled.
Events
Subscribe to events per-webhook in Console → Webhooks.
| Event | Fires when | Payload |
|---|---|---|
| incident.created | A monitoring check opens a new incident (DNS failure, certificate-transparency finding, and other non-HTTP check types). | Incident |
| incident.resolved | An open incident is cleared — either the check goes healthy again, or someone resolves it in the console. HTTP incidents use monitor.up instead. |
Incident |
| monitor.down | An HTTP check starts failing for a domain. | Incident |
| monitor.up | A failing HTTP check recovers. | Incident |
| ssl.expiring | A domain's certificate enters its expiry-warning window (days remaining still > 0). | Incident |
| ssl.expired | A domain's certificate has expired. | Incident |
| domain.expiring | A domain's registration enters its expiry-warning window, or has lapsed. | Incident |
| maintenance.started | A scheduled maintenance window begins for a domain. | Maintenance |
| maintenance.finished | A maintenance window ends (or is cancelled). | Maintenance |
| assistant.analysis.completed | The Paqet Assistant finishes an analysis with a NeedsAttention or Uncertain verdict. Benign analyses do not fire this event. |
Assistant |
| incident.updated | Reserved — selectable but not currently delivered. | — |
| account.limit_reached | Reserved — selectable but not currently delivered. | — |
Event payloads
Incident
Sent for incident.created, incident.resolved, monitor.down,
monitor.up, ssl.expiring, ssl.expired, and domain.expiring.
{
"DomainId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"Host": "example.com",
"CheckType": "HTTP",
"Status": "Error",
"Title": "HTTP Error: 503 Service Unavailable",
"Summary": "The endpoint returned 503 for 3 consecutive checks."
}
| Field | Type | Description |
|---|---|---|
| DomainId | string (uuid) | The affected domain. Look it up via GET /v1/domains/{id}. |
| Host | string | The domain's hostname. |
| CheckType | string | DNS, SSL, HTTP, CertTransparency, or DomainExpiry. |
| Status | string | The check status that triggered the event — Error, Warning, or Healthy (on resolve). |
| Title | string | Human-readable incident summary. |
| Summary | string | null | Extra detail where available. |
Maintenance
Sent for maintenance.started and maintenance.finished.
{
"DomainId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"Host": "example.com",
"StartsAt": "2026-09-02T01:00:00+00:00",
"EndsAt": "2026-09-02T03:00:00+00:00",
"Reason": "Database migration"
}
| Field | Type | Description |
|---|---|---|
| DomainId | string (uuid) | The domain the window applies to. |
| Host | string | The domain's hostname. |
| StartsAt | string (ISO 8601) | Window start, UTC. |
| EndsAt | string (ISO 8601) | Window end, UTC. |
| Reason | string | null | Optional operator note. |
Assistant analysis
Sent for assistant.analysis.completed.
{
"AnalysisId": "d4a1c9f0-6b2e-4a1d-9c3f-1e2d3c4b5a60",
"OrganizationId": "7b2c1a90-3f4e-4d5c-8a1b-2c3d4e5f6a7b",
"CycleDate": "2026-09-01",
"TriggerType": "Scheduled",
"Verdict": "NeedsAttention",
"Confidence": "High",
"Severity": "Medium",
"Headline": "Unexpected certificate issuance for api.example.com",
"Reasoning": "A certificate for api.example.com was issued by a CA this organization has not used before...",
"SignalCount": 3,
"ConsoleUrl": "https://app.paqet.io/console/assistant?open=d4a1c9f0-6b2e-4a1d-9c3f-1e2d3c4b5a60"
}
| Field | Type | Description |
|---|---|---|
| AnalysisId | string (uuid) | Fetch the full analysis via GET /v1/assistant/analyses/{id}. |
| OrganizationId | string (uuid) | The organization the analysis belongs to. |
| CycleDate | string (date) | The daily cycle the analysis covers (YYYY-MM-DD). |
| TriggerType | string | Scheduled (the daily pass) or OnDemand (a re-run). |
| Verdict | string | NeedsAttention or Uncertain — see below. |
| Confidence | string | null | Low, Medium, or High. |
| Severity | string | null | Info, Low, Medium, High, or Critical. |
| Headline | string | null | One-line summary of the finding. |
| Reasoning | string | null | The Assistant's full explanation (up to ~4,000 characters). |
| SignalCount | number | How many signals the analysis reviewed. |
| ConsoleUrl | string | Deep link that opens this analysis in the console. |
Assistant verdicts, confidence & severity
The Assistant reviews monitoring signals and produces three graded fields, used on both the webhook payload and the Assistant API.
- Verdict —
Benign(matches this organization's established patterns; no webhook is sent),NeedsAttention(review this),Uncertain(the Assistant could not confidently rule it benign — treat it as "look, but don't panic", not as a confirmed problem). - Confidence —
Low/Medium/High: how sure the Assistant is of that verdict. - Severity —
Info/Low/Medium/High/Critical: how urgent the finding is if it is real.
Slack & Discord
Webhooks with a Slack or Discord destination receive that platform's native message format
(Block Kit / embeds) instead of the signed envelope, so they render as a readable message in-channel.
The same X-Paqet-* headers are still sent and still signed. If you want the raw JSON envelope,
use a Generic destination.
Set up a webhook
Create one, pick its events, and send a test delivery from the console.