Skip to content

Webhooks

Webhooks let you push real-time event notifications from Breeze to any external HTTP endpoint. When a device enrolls, an alert fires, a script completes, or any other tracked event occurs, Breeze sends a signed JSON payload to each webhook configured for that event type. Webhooks belong to an organization or a partner and support custom headers, HMAC-SHA256 signature verification, automatic retries on a fixed schedule, and a full delivery history with per-delivery status tracking.


  1. Choose the events you want to subscribe to (see Event Types below).

  2. Send a POST request with the webhook name, destination URL, signing secret, and event list:

    Terminal window
    POST /webhooks
    Content-Type: application/json
    {
    "name": "PagerDuty Alerts",
    "url": "https://events.pagerduty.com/integration/abc123/enqueue",
    "secret": "whsec_your_signing_secret",
    "events": ["alert.triggered", "alert.acknowledged", "device.offline"],
    "payloadVersion": 2,
    "headers": [
    { "key": "X-Custom-Source", "value": "breeze-rmm" }
    ]
    }
  3. The API validates the URL against the SSRF safety rules (see URL Safety Validation — HTTPS to a publicly routable address on hosted Breeze; self-hosted deployments may also target private addresses), encrypts the secret, and returns the new webhook:

    {
    "id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
    "orgId": "11111111-1111-1111-1111-111111111111",
    "name": "PagerDuty Alerts",
    "url": "https://events.pagerduty.com/integration/abc123/enqueue",
    "events": ["alert.triggered", "alert.acknowledged", "device.offline"],
    "payloadVersion": 2,
    "headers": [{ "key": "X-Custom-Source", "value": "breeze-rmm" }],
    "status": "active",
    "hasSecret": true,
    "createdAt": "2026-02-18T12:00:00.000Z",
    "updatedAt": "2026-02-18T12:00:00.000Z",
    "lastDeliveryAt": null
    }
  4. Send a test delivery to verify connectivity (see Testing a Webhook).

Field Type Required Meaning
name string Yes Webhook name
url string Yes Destination URL
events string[] Yes Subscribed event types; * receives all events
payloadVersion 1 or 2 No Payload version; defaults to 2
secret string With HMAC Signing secret; use a Bearer token header instead when appropriate
headers array of key/value objects No Custom request headers
ownerScope organization or partner No Owner of the webhook; defaults to organization
orgId UUID string For organization webhooks created in partner or system scope Organization that owns the webhook; omit for partner webhooks
partnerId UUID string For partner webhooks created in system scope Partner that owns the webhook; partner sessions use their authenticated partner

Webhooks belong to one organization or to a partner. Organization webhooks receive that organization’s events. Organization sessions use their authenticated organization; partner and system sessions supply orgId when creating an organization webhook.

Partner administrators with access to all of their partner’s organizations can choose All organizations when creating a webhook (ownerScope: "partner", without orgId). System sessions also supply partnerId. Ownership cannot be changed after creation. Organization users and partner users with access to selected organizations cannot see partner webhooks or their delivery history.

Partner webhooks receive events from every live organization belonging to their partner. Holding organizations (the unassigned pool), quick-support organizations, and suspended or deleted organizations are excluded. The delivery envelope’s orgId identifies the event’s organization. Receivers can filter on that value; this release has no per-organization subscription filter. If an organization leaves the partner, its queued deliveries are stopped before sending.

New partner webhooks default to payload v2. Their v2 alert payloads include orgExternalIds, which lets receivers map the organization to external systems. Test sends from partner webhooks carry orgId: null and appear as Test in delivery history.

An organization’s tenant export includes delivery metadata for its own events sent through partner webhooks. The stored payload and receiver response text are excluded, as are the partner’s webhook configuration and secrets.


Webhooks subscribe to events emitted by the Breeze event bus. Pass one or more event type strings in the events array when creating or updating a webhook. Use * to receive every event. The API rejects an event type that Breeze never publishes with a 400, so a typo cannot save a webhook that silently never fires. The legacy name alert.created is accepted and stored as alert.triggered.

Event Description
device.enrolled An agent has completed enrollment (first enrollment or re-enrollment; payload reenrollment distinguishes them)
device.online A device has come online (heartbeat received)
device.offline A device has stopped sending heartbeats
device.updated Device metadata or configuration has changed
device.reinstalled A reinstalled machine was reattached to its existing device record: the record keeps its id, org, site and history, a new install takes it over, and the install incarnation advances. Payload: deviceId, sourceDeviceId, orgId, sourceOrgId, siteId, installIncarnation, previousIncarnation, evidenceLevel, actorUserId, reattachmentId
Event Description
alert.triggered A monitoring rule has fired a new alert
alert.acknowledged An alert has been acknowledged by a user or service principal
alert.resolved An alert condition has cleared
alert.suppressed An alert has been suppressed: from the dashboard, POST /alerts/:id/suppress, the bulk action (POST /alerts/bulk), or Breeze AI
alert.escalated An escalation policy step fired for an alert that is still active. One event per alert per step occurrence (each renotify repeat is its own occurrence), however many channels and users the step pages
Event Description
script.started Script execution has begun on a device
script.completed Script execution finished successfully
script.failed Script execution failed or timed out (a cancelled run emits neither)
Event Description
automation.started An automation workflow has begun
automation.completed An automation workflow completed successfully
automation.failed An automation workflow failed
Event Description
policy.evaluated A configuration policy has been evaluated against a device
policy.violation A device is non-compliant with a policy
policy.compliant A device has returned to compliance
Event Description
patch.available New patches are available for a device
patch.approved A patch has been approved for installation
patch.installed A patch was installed successfully
patch.failed A patch installation failed
patch.rollback A patch has been rolled back

Ticket events are published from the ticketing transactional outbox with id-only payloads: identifiers, enum labels and field names, never a subject, description, resolution note or comment body. Read the full record through the API (staff JWT, or the Partner API tickets:read feed) when you need it. Rely only on the fields listed below: a payload can also carry other internal identifiers, which are not part of this contract.

Event Payload (besides ticketId) Description
ticket.created internalNumber, source, assigneeId, actorPrincipalId A ticket was opened (from the portal, email, an alert, the UI, the Partner API or AI). actorPrincipalId is the service principal that opened it (null otherwise)
ticket.commented commentId, isPublic, originPrincipalKind, originPrincipalId A comment, note or feed entry was added. originPrincipalKind and originPrincipalId are the comment’s stored origin, the same values the Partner API feed returns for it: service_principal with the principal’s id, ai_agent with the agent’s id, user for people, and system for Breeze’s own system notes, such as caller verification (originPrincipalId is null for the last two). A comment Breeze writes on a customer’s behalf is user by design: a reply that arrives by email (or a public email linked from the Outlook add-in) and the reopen that reply triggers. A mirroring integration skips exactly the comments it wrote by matching service_principal and its own principal id
ticket.status_changed from, to, statusId The core status changed (statusId is the partner’s custom status row, when one was used)
ticket.updated changed (field names) Fields were edited — subject, description, priority, category, due date, device, tags, requester
ticket.assigned assigneeId (null when unassigned), actorPrincipalId The assignee changed. actorPrincipalId is the service principal that made the change (null for people and the system), so a mirroring integration can skip its own assignments
ticket.merged sourceTicketId, targetTicketId, sourceInternalNumber, targetInternalNumber A ticket was merged into another. Sent once, on the surviving ticket (ticketId = targetTicketId). The source stays as a read-only record: a Partner API read of it returns mergedIntoTicketId and mergedAt. The source also gets a ticket.status_changed to closed when it was not already closed, and the survivor a ticket.updated with changed: ["merge"]. Comments moved by the merge are not re-sent as ticket.commented
ticket.sla_breached assigneeId A response or resolution SLA target was missed

A ticket migration window lets a partner admin import ticket history from another system without notifying anyone or firing per-ticket events. Nothing the import writes produces a ticket event. When the window closes, each organization in it gets one summary event.

Event Payload Description
migration.window_closed windowId, partnerId, sourceLabel, openedAt, endsAt, closedAt, closedReason, ticketsImported, commentsImported, attachmentsImported A migration window closed. Sent once per organization in the window, with that organization’s counts. attachmentsImported counts the files attached to imported comments. closedReason is expired, closed_by_admin, principal_inactive or opener_inactive. The event id is <windowId>:<orgId>, so a retried delivery is recognizable

Integration events report the health of an organization’s own connections: an integration, a sync or an OAuth grant. A health incident opens when a connection breaks and resolves when it recovers or is retired.

Event Payload Description
integration.health_opened incidentId, orgId, sourceKind, sourceId, condition, severity, fixHref, openedAt, resolvedAt, resolution A connection health incident opened. resolvedAt and resolution are null
integration.health_resolved incidentId, orgId, sourceKind, sourceId, condition, severity, fixHref, openedAt, resolvedAt, resolution A connection health incident resolved
Field Type Description
incidentId string (uuid) The incident. Both events for one incident carry the same value
orgId string (uuid) The organization that owns the connection
sourceKind string The kind of connection, for example m365_connection, dns_filter or webhook
sourceId string or null The connection’s id. null for oauth_grant, because that id embeds a user id
condition string A fixed code for what is wrong, for example sync_failed or reauth_required
severity string permanent (needs someone to act) or transient (may clear on its own)
fixHref string A relative path in the Breeze web app where the connection can be repaired
openedAt string (ISO timestamp) When the incident opened
resolvedAt string (ISO timestamp) or null When the incident resolved. null while it is open
resolution string or null recovered or retired. null while the incident is open
  • No free text. The payload never contains provider error text or the connection’s display name. Use fixHref and the API to look up more.
  • Organization connections only. Only incidents about an organization’s own connections emit events. Partner-wide connections do not.
  • Level-triggered. Events follow the state seen at each health check, about every 15 minutes. A connection that breaks and recovers between two checks may emit nothing. A consumer can also receive an integration.health_resolved event for an incident it never saw open, for example an incident that existed before the feature shipped.
  • Stable event ids. The event id is stable for a given incident and state, so retried deliveries are deduplicated.
  • No self-delivery. An incident about a webhook is never delivered to that same webhook.
Event Description
security.score_changed A device’s security posture score has changed
remote.session.started A remote access session has begun
remote.session.ended A remote access session has ended
user.login A user logged in
user.logout A user logged out
session.login A device user session started
session.logout A device user session ended

When an event is emitted on the Breeze event bus, the webhook delivery system routes it to subscribed active webhooks owned by that organization and, for live organizations, its partner. Each delivery follows this sequence:

  1. The event bus fires the event. The webhook subscriber matches the event type against eligible organization and partner webhooks.

  2. A delivery record is created in the webhook_deliveries table with status pending and attempts set to 0.

  3. A BullMQ job carrying only the delivery and webhook ids, generation, and attempt count is queued on webhook-delivery.

  4. The worker claims the delivery row as retrying, validates the destination URL (including DNS resolution to block SSRF), and sends an HTTP POST with the signed payload.

  5. A 2xx response marks the row delivered. A failure marks it scheduled with the next attempt time, or failed after the last attempt.

The original v1 JSON POST body has this structure. See Payload versions for the v2 envelope.

{
"id": "event-uuid",
"type": "device.offline",
"timestamp": "2026-02-18T12:34:56.789Z",
"orgId": "11111111-1111-1111-1111-111111111111",
"data": {
"deviceId": "abc123",
"hostname": "WORKSTATION-01",
"lastSeen": "2026-02-18T12:30:00.000Z"
}
}
Field Description
id Unique event ID (UUID)
type The event type string (e.g., alert.triggered)
timestamp ISO 8601 timestamp of when the event was created
orgId The organization the event belongs to; null for partner-webhook test sends
data Event-specific payload; contents vary by event type

Each webhook has a payloadVersion: 1 or 2. Webhooks created before this release stay on 1; their payload is unchanged byte for byte. New webhooks default to 2. Webhooks created through the API without payloadVersion also default to 2.

Choose the version in the edit form, or send PATCH /webhooks/:id with { "payloadVersion": 2 }. You can switch back to 1. Each delivery keeps the payload version it was recorded at, including manual replays and legacy rows. Editing the webhook stops its scheduled retries when they next come due.

The v2 envelope is { id, type, timestamp, orgId, schemaVersion: 2, replayOf, data }, in that order. The v1 envelope has no schemaVersion or replayOf. For non-alert events, v2 changes only the envelope; data is the same as v1. A test delivery (POST /webhooks/:id/test) uses the webhook’s version with sample data; it does not enrich that sample.

Only alert.triggered, alert.acknowledged, alert.resolved, alert.suppressed, and alert.escalated use the v2 alert data below. Other event types keep their original data.

Field Type Meaning
alertId string Alert id
ruleId string or null Rule id; null for rule-less or sourced alerts
deviceId string Device id
severity string Alert severity
title string Alert title
message string Alert message
monitorId string or null Always present on alert-service paths; null when there is no monitor
kind string or null Always present on alert-service paths; null when there is no monitor
episodeId string or null, when present Episode id for the subject-key path
subjectKey string or null, when present Subject identity; null on the ordinary rule-alert path
responsesOwner boolean, when present Whether this alert owns the response actions
source string, when present Source of a rule-less or sourced alert
Producer-specific extra keys varies Some producers add extra keys such as networkChangeEventId, vendor, connectionId, provider, detectionId, requiresHuman, automationId, runId, flowRunId, and anomalyId
Field Type Meaning
alertId string Alert id
ruleId string or null Rule id
deviceId string Device id
acknowledgedBy string or null Id of the acknowledging user; null for a service principal
acknowledgedByPrincipalId string (optional) Partner API service principal id; present only for a principal acknowledgment

Several paths publish this event: the alert service, REST resolve, bulk actions, mobile, correlation-group actions, Breeze AI, warranty checks, and the subject-alert outbox. Fields vary by producer.

Field Type Meaning
alertId string Alert id; always present
ruleId string or null, when present Rule id; omitted by some producers
deviceId string Device id; always present
resolutionNote string or null, when present Resolution note; omitted by some producers
resolvedAt ISO 8601 string Resolution time; always present
resolvedBy string or null Resolving user id; always present, null for system or service principal resolutions
resolvedByPrincipalId string (optional) Partner API service principal id; present only for a principal resolution, which counts as a person’s resolve and starts no automatic AI verdict run
triggeredAt ISO 8601 string Trigger time; always present
resolutionReason string or null, when present Resolution reason; alert-service paths only
subjectKey string, when present Subject identity; subject recoveries only

Every suppress path publishes this event with the same data: POST /alerts/:id/suppress, the bulk action, and Breeze AI. It is published only when the suppression was written; a request that lost a race to a resolve or dismiss publishes nothing. Re-suppressing an already suppressed alert (to change its end time) publishes again.

Field Type Meaning
alertId string Alert id
ruleId string or null Rule id
deviceId string Device id
suppressedBy string Suppressing user id
suppressedUntil ISO 8601 string or null Suppression end time; null for indefinite suppression
durationHours number or null Suppression duration in hours: the duration Breeze AI was given, or the time from the request to suppressedUntil (2 decimals) for an end-time suppression. Null for indefinite suppression

Published when an escalation policy step fires. The step fires only while the alert is active: if the alert was acknowledged, suppressed, resolved, or dismissed before the step came due, no event is sent. A step none of whose channels or users is still valid sends nothing and publishes nothing. A retried publish reuses the event id, so deduplicate on it.

Field Type Meaning
alertId string Alert id
ruleId string or null Rule id
deviceId string Device id
escalationPolicyId string Escalation policy id
escalationStep number Occurrence id: policyStep + 10 * repeat
policyStep number Position of the step in the policy, 1 to 10
repeat number Renotify repeat, 0 for the step’s first firing

Every key below is always present. A value that does not apply or is unavailable is null. The status comes from the event type, even if the alert has since changed. Valid payload values take precedence over the current alert row.

externalTickets is also captured when the delivery is first enriched. A reference written afterward is absent from that stored payload, and retries do not refresh it. For current state, call GET /api/v1/partner-api/alerts/<alertId>/external-tickets with alerts:read (or the staff GET /api/v1/alerts/<alertId>/external-tickets route). V1 payloads are unchanged.

Visibility follows webhook ownership. Partner-owned webhooks receive every partner-principal reference on the alert plus mappings from PSA connections owned by that partner or by the alert’s organization. Org-owned webhooks receive only mappings from PSA connections owned by the alert’s organization with no partner owner. They never receive partner-principal references or partner-owned PSA connection mappings. Org-scoped users of the staff GET route receive the same restricted view as an org-owned webhook.

Hostname, site, and organization name are read when the delivery is first recorded, not at the moment the event happened. Retries resend the stored payload unchanged. enrichmentIncomplete: true means the alert row could not be read (it was not yet committed, the id was invalid, or the enrichment query failed), so enriched fields may be null; look the alert up by alertId. A missing device or site leaves its fields null without setting the flag.

Alerts can belong to a device, a topology site, or a SaaS backup workload. Workload alerts keep deviceId: null; use subjectName for display, for example Microsoft 365 mail backup · Example customer. Organization-owned webhook labels honor the connection’s portal provider-name setting. The owner display fields below are additive; existing event fields keep their previous meanings, and an event’s owner and ownerKey extras remain available in details.

Field Type Meaning
alertId string or null Alert id
ruleId string or null Rule id
monitorId string or null Monitor id
deviceId string or null Device id
owner object or null { kind, ref }; kind is device, topology_site, or backup_workload, and ref is the owner id or null when unavailable
ownerKind string or null Owner kind; null when neither the row nor event identifies the owner
ownerLabel string or null Display label for the owner, such as Microsoft 365 mail backup
subjectName string or null Display subject, including the customer name for a backup workload
episodeId string or null Alert episode id
status active, acknowledged, resolved, or suppressed State represented by this event; alert.triggered and alert.escalated mean active
severity critical, high, medium, low, info, or null Alert severity
title string or null Alert title
message string or null Alert message
triggeredAt ISO 8601 string or null Trigger time
acknowledgedAt ISO 8601 string or null Acknowledgment time
resolvedAt ISO 8601 string or null Resolution time
deviceHostname string or null Device hostname
siteId string or null Topology alert’s owning site, otherwise the device’s site
siteName string or null Site name
orgName string or null Organization name
alertUrl string Dashboard link to the alert, or /alerts when the alert id is unavailable; relative when no dashboard base URL is configured
externalTickets array of objects References and PSA mappings permitted for the webhook owner at enrichment time (org-owned webhooks receive only org-owned PSA mappings). Each object has exactly source (partner_api or psa), externalId, nullable externalUrl, and nullable externalNumber; empty when none exist
acknowledgedByPrincipalId string or null Partner API service principal that acknowledged the alert; null when absent
resolvedByPrincipalId string or null Partner API service principal that resolved the alert; null when absent. A principal resolution counts as a person’s resolve and starts no automatic AI verdict run
enrichmentIncomplete boolean The alert row could not be read or the enrichment query failed; missing device or site alone does not set it
subjectKey string or null Alert subject identity
resolutionNote string or null Resolution note
resolvedBy string or null Resolving user id
resolutionReason string or null Resolution reason
acknowledgedBy string or null Acknowledging user id
suppressedBy string or null Suppressing user id from the event
suppressedUntil ISO 8601 string or null Suppression end time
responsesOwner boolean or null Whether this alert owns response actions, when supplied by the event
kind string or null Monitor or producer kind from the event
source string or null Alert source from the event
details object Producer-specific extras, such as durationHours or networkChangeEventId. These keys are not a stable contract; {} when there are none

A complete v2 alert.triggered example:

{
"id": "60ecc32d-4bde-4716-930d-a433a86836aa",
"type": "alert.triggered",
"timestamp": "2027-01-12T12:34:56.000Z",
"orgId": "11111111-1111-4111-8111-111111111111",
"schemaVersion": 2,
"replayOf": null,
"data": {
"alertId": "22222222-2222-4222-8222-222222222222",
"ruleId": "33333333-3333-4333-8333-333333333333",
"monitorId": "44444444-4444-4444-8444-444444444444",
"deviceId": "55555555-5555-4555-8555-555555555555",
"owner": {
"kind": "device",
"ref": "55555555-5555-4555-8555-555555555555"
},
"ownerKind": "device",
"ownerLabel": "WORKSTATION-01",
"subjectName": "WORKSTATION-01",
"episodeId": null,
"status": "active",
"severity": "high",
"title": "Low disk space",
"message": "Available disk space is below 10%.",
"triggeredAt": "2027-01-12T12:34:56.000Z",
"acknowledgedAt": null,
"resolvedAt": null,
"deviceHostname": "WORKSTATION-01",
"siteId": "66666666-6666-4666-8666-666666666666",
"siteName": "Main office",
"orgName": "Example Organization",
"alertUrl": "https://app.example.com/alerts/22222222-2222-4222-8222-222222222222",
"externalTickets": [],
"acknowledgedByPrincipalId": null,
"resolvedByPrincipalId": null,
"enrichmentIncomplete": false,
"subjectKey": null,
"resolutionNote": null,
"resolvedBy": null,
"resolutionReason": null,
"acknowledgedBy": null,
"suppressedBy": null,
"suppressedUntil": null,
"responsesOwner": true,
"kind": "disk",
"source": null,
"details": {}
}
}

A manual retry creates a new event with a new id. It carries X-Breeze-Replay-Of: <original event id> in either version, and replayOf in the v2 body. A replay of a replay keeps the original event id as its root.

Deduplicate transport on the original event id: replayOf ?? id for v2, or X-Breeze-Replay-Of falling back to id for v1. Deduplicate tickets on alertId plus your own ticket state, not on alertId + type: a suppressed alert can return to active and be acknowledged again.

Deliveries use these headers; signature and replay headers apply as noted:

Header Description
Content-Type application/json
User-Agent Breeze-Webhooks/1.0
X-Breeze-Delivery-Id Unique ID for this delivery attempt
X-Breeze-Event-Id The event ID from the payload
X-Breeze-Event-Type The event type string
X-Breeze-Timestamp Unix timestamp (milliseconds) of the delivery attempt
X-Breeze-Signature HMAC-SHA256 signature (only if a secret is configured)
X-Breeze-Signature-Timestamp Timestamp used in the signature computation
X-Breeze-Replay-Of Original event id on a manual retry, in either payload version; absent on original deliveries

Any custom headers configured on the webhook are merged into the request after the standard headers.


Every non-2xx response, timeout, or transport error is retried on this fixed schedule. Each request has a 30-second timeout.

Attempt Delay after the previous attempt
1 Immediately
2 1 minute
3 5 minutes
4 30 minutes
5 2 hours
6 6 hours
7 12 hours

Each delay has ±20% jitter. The base schedule spans about 21 hours from the first attempt to the last; a seventh failure leaves the delivery failed. The per-webhook retry_policy is ignored.

Retry state lives in Postgres, with BullMQ delayed jobs waking each attempt. A wake-up lost to a failed enqueue or to Redis data loss is re-driven by the recovery sweep once the delivery is more than 15 minutes overdue. Each attempt is claimed before sending, so two workers cannot send the same attempt. If a worker stops mid-request, recovery marks the delivery failed with an unknown outcome instead of re-sending a potentially delivered request.

Editing or disabling a webhook stops its scheduled retries; each is marked failed (superseded_by_edit:) when it next comes due, including retries re-driven by the recovery sweep. Replay it manually if needed. A delivery keeps the payload version it was recorded at; this also applies to manual replays and legacy rows.

The Redis dead-letter queue is retired. On first start after upgrading, Breeze drains the old Redis delivery list and DLQ: pending rows and old in-memory retries are re-driven; entries whose delivery no longer exists or already finished are dropped and counted. DLQ entries become failed deliveries available for manual replay when their webhook still exists. Entries that fail to process stay in Redis and are retried by the recovery sweep.


If your endpoint expects a static token rather than a signature, authenticate with a bearer token instead of a signing secret. In the dashboard, choose Bearer token under Authentication. Through the API, send the token as an Authorization custom header and omit secret:

{
"name": "Ticketing intake",
"url": "https://intake.example.com/breeze",
"events": ["ticket.created"],
"headers": [
{ "key": "Authorization", "value": "Bearer your_token" }
]
}

The token is stored encrypted with the other custom headers and is never returned: GET responses show the header with a redacted value and report "authType": "bearer" ("hmac" for a signing secret, "none" for neither). A webhook needs either a secret or an Authorization: Bearer <token> header; other Authorization schemes (such as Basic) can be added as custom headers alongside a signing secret. To switch an existing webhook from HMAC to bearer, send "secret": null together with the Authorization header in the same PATCH. To keep a stored token while you edit other headers, send its value back as ********.


When a webhook has a signing secret, every delivery includes an HMAC-SHA256 signature in the X-Breeze-Signature header. Use this to verify that the payload was sent by Breeze and has not been tampered with.

The signature input is the concatenation of the timestamp and the raw JSON payload body, separated by a dot:

signature_input = timestamp + "." + payload_body
signature = HMAC-SHA256(secret, signature_input)

The X-Breeze-Signature header value is prefixed with sha256=:

X-Breeze-Signature: sha256=a1b2c3d4e5f6...

The X-Breeze-Signature-Timestamp header contains the same timestamp value used in the signature computation. The signature covers the raw body for both payload versions.

import { createHmac, timingSafeEqual } from 'crypto';
function verifyWebhookSignature(payload, secret, signatureHeader, timestampHeader) {
const expectedSig = signatureHeader.replace('sha256=', '');
const signatureInput = `${timestampHeader}.${payload}`;
const computedSig = createHmac('sha256', secret)
.update(signatureInput)
.digest('hex');
return timingSafeEqual(
Buffer.from(expectedSig, 'hex'),
Buffer.from(computedSig, 'hex')
);
}

Example: alert to ConnectWise Manage through middleware

Section titled “Example: alert to ConnectWise Manage through middleware”

This outline uses a low-code automation platform such as Rewst between Breeze and ConnectWise Manage. Adapt the fields and priority mapping to your Manage configuration.

  1. Create a v2 webhook subscribed to alert.triggered, alert.acknowledged, and alert.resolved. Point it at the middleware’s incoming webhook URL.
  2. Verify the request before processing it. Use the HMAC recipe above with X-Breeze-Signature-Timestamp and the raw body. If the platform can only check a static header, use Breeze’s Bearer token option and validate the Authorization header instead.
  3. Deduplicate received requests on replayOf ?? id. Store the key after processing succeeds so a failed middleware attempt can be retried.
  4. On alert.triggered, map data.severity to your Manage priority ids. For example, map critical and high to your urgent queue, and send null severity for review. Use data.orgName to look up the company, data.deviceHostname and data.title in the ticket summary, and data.alertUrl in the description. Keep the lookup explicit when organization names are ambiguous or enrichment is incomplete.
  5. Store an alertId → ticket id mapping. On alert.acknowledged, update the mapped ticket according to your workflow. Use the alert id and current ticket state to decide whether to create or update; repeated alert states must not create duplicate tickets.
  6. On alert.resolved, close the mapped ticket with your configured Manage closed status. If no mapping exists, send the event for review rather than creating a new closed ticket.
  7. Grant a dedicated principal alerts:write. When the Manage ticket closes, call POST /api/v1/partner-api/alerts/<alertId>/resolve with an optional note. Ignore webhook events whose acknowledgedByPrincipalId or resolvedByPrincipalId is your own principal id so the write-back does not loop.

Every webhook maintains a paginated delivery history. Each delivery record captures the event type, payload, response details, and retry state. The list returns status and timing only; the event payload and the receiver’s response body are not included. Reading the history requires webhooks:read.

From v0.124.0, delivered and failed records are kept for 90 days by default and then removed by a nightly job; records that are still pending, scheduled or retrying are never removed. Self-hosters can change the window with the webhook delivery retention variables in Environment Variables. A webhook whose delivery ends failed after its last attempt, with no later success, opens a connection-health incident on Integrations. For a dead endpoint, that is about 21 hours after the first failed attempt, rather than minutes. Decrypt failures open an incident immediately. See Connection Health Checks.

Terminal window
GET /webhooks/:id/deliveries?page=1&limit=50&status=failed

All query parameters are optional. Results are ordered by most recent first.

Parameter Type Description
page integer Page number (default: 1)
limit integer Results per page (default: 50, max: 100)
status string Filter by delivery status: pending, scheduled, delivered, failed, retrying

Response:

{
"data": [
{
"id": "delivery-uuid",
"webhookId": "webhook-uuid",
"orgId": "org-uuid",
"status": "delivered",
"event": "alert.triggered",
"eventId": "event-uuid",
"responseStatus": 200,
"attempt": 1,
"nextAttemptAt": null,
"createdAt": "2026-02-18T12:34:56.000Z",
"deliveredAt": "2026-02-18T12:34:57.123Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 142
}
}

When you fetch a single webhook via GET /webhooks/:id, the response includes aggregated delivery statistics:

{
"id": "webhook-uuid",
"name": "PagerDuty Alerts",
"payloadVersion": 2,
"status": "active",
"deliveryStats": {
"total": 1284,
"pending": 2,
"delivered": 1250,
"failed": 30,
"retrying": 2,
"scheduled": 0,
"lastDeliveredAt": "2026-02-18T12:34:57.123Z"
}
}

You can manually retry any delivery that has a failed status. The retry creates a new delivery record with the same event type and payload, and queues it for immediate processing. The endpoint returns 202 Accepted even when queuing fails: the delivery stays pending, and the recovery sweep sends it within about 15–20 minutes.

Terminal window
POST /webhooks/:id/retry/:deliveryId

Response (202 Accepted):

{
"message": "Delivery retry queued",
"delivery": {
"id": "new-delivery-uuid",
"status": "pending",
"attempt": 0
}
}

Send a test delivery to verify that your endpoint is reachable and correctly processing payloads:

Terminal window
POST /webhooks/:id/test
Content-Type: application/json
{
"event": "webhook.test",
"payload": {
"message": "Hello from Breeze RMM"
}
}

Both event and payload are optional. If omitted, the test uses event type webhook.test and a default payload containing a message, timestamp, and webhook ID.

Response (202 Accepted):

{
"message": "Test delivery queued",
"delivery": {
"id": "delivery-uuid",
"webhookId": "webhook-uuid",
"status": "pending",
"event": "webhook.test",
"attempt": 0
}
}

The test endpoint returns 202 Accepted even when queuing fails: the delivery stays pending, and the recovery sweep sends it within about 15–20 minutes. The AI test_webhook tool likewise reports the delivery as accepted when queuing fails. A test delivery uses the same HMAC signing, URL validation, and delivery pipeline as real events; a failing endpoint follows the full retry schedule.


Update any combination of name, URL, secret, events, headers, status, or payload version:

Terminal window
PATCH /webhooks/:id
Content-Type: application/json
{
"events": ["device.enrolled", "device.offline", "alert.triggered"],
"payloadVersion": 2,
"status": "paused"
}
Request field Type Meaning
name string Webhook name
url string Destination URL
events string[] Replacement subscription list
payloadVersion 1 or 2 Version for future deliveries; stored deliveries keep their recorded version
secret string or null Signing secret; null removes it when switching to Bearer authentication
headers array of key/value objects Replacement custom headers
status string One of the values below

All update fields are optional.

Status Value Description
active Webhook is enabled and will receive deliveries
paused Webhook is temporarily disabled; no deliveries are sent
failed Webhook has been marked as failed (can be set manually or by the system)

Terminal window
DELETE /webhooks/:id

Breeze validates webhook URLs at both creation time and delivery time to prevent SSRF (Server-Side Request Forgery) attacks.

At creation and update time, the API performs synchronous validation including DNS resolution. On hosted Breeze:

  • URL must use HTTPS
  • Hostname must not be localhost, *.localhost, or *.local
  • IP addresses must not be private, loopback, or link-local (covers RFC 1918, RFC 6598, RFC 5737, and IPv6 equivalents)
  • DNS resolution is performed to catch hostnames that resolve to blocked address ranges

Self-hosted deployments (IS_HOSTED=false) may point webhooks at private RFC 1918/ULA addresses on their own network — an on-prem SIEM or ticketing system, for example — and plain http:// is permitted only when the target resolves to such a private address. Public destinations still require HTTPS, and cloud-metadata, loopback, and link-local addresses are blocked in every configuration.

At delivery time, the worker re-validates the URL before every delivery attempt. If the URL has become unsafe (e.g., DNS now resolves to a private IP), the delivery fails immediately without making an HTTP request.


Method Path Description
GET /webhooks List accessible webhooks (paginated, filterable by status, ownerScope, and orgId; an orgId filter returns only that organization’s own webhooks)
POST /webhooks Create a new webhook
GET /webhooks/:id Get webhook details including delivery statistics
PATCH /webhooks/:id Update webhook name, URL, secret, events, headers, status, or payload version
DELETE /webhooks/:id Delete a webhook and all its delivery records
GET /webhooks/:id/deliveries List delivery history (paginated, filterable by status)
POST /webhooks/:id/test Send a test delivery
POST /webhooks/:id/retry/:deliveryId Retry a failed delivery

All webhook routes require authentication and are scoped to organization, partner, or system roles. They also require a permission:

  • Reading (GET /webhooks, GET /webhooks/:id, GET /webhooks/:id/deliveries) requires webhooks:read. The AI assistant’s query_webhooks tool requires the same permission.
  • Managing (create, update, delete, test, retry) requires organizations:write.

webhooks:read is granted by default to Org Admin, Org Technician and Partner Technician; Partner Admin holds it through *:*. In the dashboard, a user without webhooks:read does not see the Webhooks tab under Integrations, and a user without organizations:write sees webhooks and their delivery history without the create, edit, delete, test, pause/resume or retry controls. See Users & Roles to grant either permission to a role.


Column Type Description
id UUID Primary key
org_id UUID Organization this webhook belongs to
name varchar(255) Display name
url text Destination URL (HTTPS; self-hosted deployments may use HTTP to private addresses); encrypted at rest using AES. GET responses strip any credentials embedded in the URL (HTTP basic-auth userinfo and signed query tokens) before returning it.
secret text Encrypted HMAC signing secret (AES-256-GCM)
events text[] Array of subscribed event type strings
payload_version smallint 1 or 2, enforced by a CHECK; existing webhooks stay on 1, new webhooks default to 2
headers JSONB Custom headers sent with each delivery
status enum active, disabled, or error
retry_policy JSONB Retired; ignored by the fixed retry schedule
success_count integer Cumulative successful deliveries
failure_count integer Cumulative failed deliveries
last_delivery_at timestamp Timestamp of the most recent delivery attempt
last_success_at timestamp Timestamp of the most recent successful delivery
created_by UUID User who created the webhook
created_at timestamp Creation timestamp
updated_at timestamp Last modification timestamp
Column Type Description
id UUID Primary key
webhook_id UUID Foreign key to webhooks
event_type varchar(100) The event type that triggered this delivery
event_id varchar(100) The unique event ID
payload JSONB The stored event data sent to the endpoint
payload_version smallint Recorded payload version; default 1 for legacy rows. A NOT VALID CHECK permits only 1 or 2 for new or updated rows without scanning the existing table
event_timestamp timestamptz, nullable Stored wire event timestamp; legacy null values fall back to delivery creation time
replay_root_event_id varchar(100), nullable Original event id for manual replays; null for original deliveries
status enum pending (awaiting first attempt), scheduled (awaiting retry), retrying (in flight), delivered, or failed
attempts integer Number of delivery attempts made
next_retry_at timestamp Due time (scheduled), execution lease (retrying), or the recovery sweep’s 15-minute lease on a re-driven pending row
failed_at timestamp, nullable When the delivery became failed; null on legacy rows
webhook_generation integer, nullable Webhook approval generation when recorded; retries stop after an edit. Legacy null rows fall back to the webhook’s current generation
response_status integer HTTP response status code from the endpoint
response_body text Response body (truncated to 1,000 characters)
response_time_ms integer Round-trip time in milliseconds
error_message text Error description if the delivery failed
created_at timestamp When the delivery record was created
delivered_at timestamp When the delivery was successfully completed

“Invalid webhook URL” error when creating a webhook. On hosted Breeze, the URL must use HTTPS and must not resolve to a private, loopback, or link-local IP address (on self-hosted deployments, private RFC 1918/ULA targets are allowed — see URL Safety Validation). Hostnames like localhost, *.localhost, and *.local are always blocked. If the hostname cannot be resolved via DNS, creation will fail.

Webhook is not receiving events. Check the webhook status – if it is paused or failed, deliveries are not processed. Verify that the events array includes the event types you expect. Use POST /webhooks/:id/test to confirm the endpoint is reachable.

Deliveries remain in scheduled status. Check nextAttemptAt: retries wait 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, then 12 hours, each with ±20% jitter. The recovery sweep re-drives deliveries more than 15 minutes overdue. After the seventh failed attempt the delivery becomes failed; use manual retry once the endpoint is repaired.

Signature verification fails on my endpoint. Ensure you are computing the HMAC over the correct input: the raw X-Breeze-Signature-Timestamp header value, followed by a dot (.), followed by the raw request body. Use the X-Breeze-Signature-Timestamp header (not X-Breeze-Timestamp) as the timestamp component. The signature uses SHA-256 and is hex-encoded.

“Unsafe webhook URL” error during delivery. The worker re-validates the URL before each delivery attempt. On hosted Breeze, if your DNS records have changed and the hostname now resolves to a private IP range, deliveries will fail — update the webhook URL to point to a publicly routable address. On self-hosted deployments, private targets are deliverable; only metadata/loopback/link-local resolutions fail.

“Only failed deliveries can be retried” error. The manual retry endpoint (POST /webhooks/:id/retry/:deliveryId) only accepts deliveries with status failed. Deliveries that are pending, scheduled, retrying, or already delivered cannot be retried through this endpoint.

“Organization context required” error. For an organization webhook created in partner or system scope, include orgId in the request body. For a partner webhook, use ownerScope: "partner" without orgId; system sessions must also provide partnerId. A list request with an orgId filter returns only that organization’s own webhooks. Omit that filter to include partner webhooks your session can manage.

Response body is truncated. Webhook delivery response bodies are truncated to 1,000 characters to prevent excessive storage consumption. If you need the full response for debugging, check your endpoint’s own logs.