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 are scoped to an organization and support custom headers, HMAC-SHA256 signature verification, automatic retries with exponential backoff, and a full delivery history with per-delivery status tracking.


Creating a Webhook

  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.escalated", "device.offline"],
    "headers": [
    { "key": "X-Custom-Source", "value": "breeze-rmm" }
    ]
    }
  3. The API validates the URL (must be HTTPS, must not resolve to private/loopback address space), 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.escalated", "device.offline"],
    "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).

Organization Scoping

Webhooks follow the standard multi-tenant access model:

Auth ScopeBehavior
OrganizationorgId is set automatically from the authenticated context.
PartnerorgId must be provided in the request body. The API verifies the partner has access to that organization.
SystemorgId must be provided in the request body.

Event Types

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.

Device Events

EventDescription
device.enrolledA new agent has completed enrollment
device.onlineA device has come online (heartbeat received)
device.offlineA device has stopped sending heartbeats
device.updatedDevice metadata or configuration has changed
device.decommissionedA device has been decommissioned

Alert Events

EventDescription
alert.triggeredA monitoring rule has fired a new alert
alert.acknowledgedAn alert has been acknowledged by a user
alert.resolvedAn alert condition has cleared
alert.escalatedAn alert has been escalated to a higher tier

Script Events

EventDescription
script.startedScript execution has begun on a device
script.completedScript execution finished successfully
script.failedScript execution failed

Automation Events

EventDescription
automation.startedAn automation workflow has begun
automation.completedAn automation workflow completed successfully
automation.failedAn automation workflow failed

Policy Events

EventDescription
policy.evaluatedA configuration policy has been evaluated against a device
policy.violationA device is non-compliant with a policy
policy.compliantA device has returned to compliance

Patch Events

EventDescription
patch.availableNew patches are available for a device
patch.approvedA patch has been approved for installation
patch.installedA patch was installed successfully
patch.failedA patch installation failed
patch.rollbackA patch has been rolled back

Other Events

EventDescription
security.score_changedA device’s security posture score has changed
remote.session.startedA remote access session has begun
remote.session.endedA remote access session has ended
remote.file.transferredA file was transferred during a remote session
user.loginA user logged in
user.logoutA user logged out
session.loginA device user session started
session.logoutA device user session ended

Delivery Mechanism

When an event is emitted on the Breeze event bus, the webhook delivery system routes it to all active webhooks in the same organization that are subscribed to that event type. Each delivery follows this sequence:

  1. The event bus fires the event. The initializeWebhookDelivery subscriber matches the event type against all configured webhooks for the organization.

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

  3. The delivery job is queued onto the Redis list breeze:webhooks:delivery.

  4. The WebhookDeliveryWorker picks up the job, validates the destination URL (including DNS resolution to block SSRF), and sends an HTTP POST with the signed payload.

  5. If the response status is 2xx, the delivery is marked delivered. Otherwise, the retry logic takes over.

Payload Format

Every webhook delivery sends a JSON POST body with the following structure:

{
"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"
}
}
FieldDescription
idUnique event ID (UUID)
typeThe event type string (e.g., alert.triggered)
timestampISO 8601 timestamp of when the event was created
orgIdThe organization the event belongs to
dataEvent-specific payload; contents vary by event type

Request Headers

Every delivery includes these headers:

HeaderDescription
Content-Typeapplication/json
User-AgentBreeze-Webhooks/1.0
X-Breeze-Delivery-IdUnique ID for this delivery attempt
X-Breeze-Event-IdThe event ID from the payload
X-Breeze-Event-TypeThe event type string
X-Breeze-TimestampUnix timestamp (milliseconds) of the delivery attempt
X-Breeze-SignatureHMAC-SHA256 signature (only if a secret is configured)
X-Breeze-Signature-TimestampTimestamp used in the signature computation

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


Retry Logic

Failed deliveries are retried automatically using exponential backoff. The default retry policy is:

ParameterDefault Value
Maximum retries5
Initial delay1,000 ms (1 second)
Backoff multiplier2x
Maximum delay300,000 ms (5 minutes)
Delivery timeout30,000 ms (30 seconds)

The delay between retries follows the formula:

delay = min(initialDelay * (multiplier ^ attempt), maxDelay)

This produces the following schedule for the default policy:

AttemptDelay
1st retry1 second
2nd retry2 seconds
3rd retry4 seconds
4th retry8 seconds
5th retry16 seconds

Custom Retry Policy

You can override the default retry policy per webhook by setting the retryPolicy field in the database. The policy object accepts:

{
"maxRetries": 10,
"initialDelayMs": 2000,
"backoffMultiplier": 3,
"maxDelayMs": 600000
}

Dead Letter Queue

After all retries are exhausted, the delivery job is moved to a dead letter queue (Redis key breeze:webhooks:dlq). Jobs in the DLQ can be inspected and retried through the worker API.


Signature Verification

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.

How the Signature Is Computed

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.

Verification Examples

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')
);
}

Delivery History

Every webhook maintains a paginated delivery history. Each delivery record captures the event type, payload, response details, and retry state.

Listing Deliveries

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

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

ParameterTypeDescription
pageintegerPage number (default: 1)
limitintegerResults per page (default: 50, max: 100)
statusstringFilter by delivery status: pending, delivered, failed, retrying

Response:

{
"data": [
{
"id": "delivery-uuid",
"webhookId": "webhook-uuid",
"orgId": "org-uuid",
"status": "delivered",
"event": "alert.triggered",
"eventId": "event-uuid",
"payload": { "alertId": "...", "severity": "critical" },
"responseStatus": 200,
"responseBody": "ok",
"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
}
}

Delivery Statistics

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

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

Retrying a Failed Delivery

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.

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

Response (202 Accepted):

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

Testing a Webhook

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 delivery is queued through the same delivery pipeline as real events, so it exercises the full path including HMAC signing, URL validation, and retry logic.


Updating a Webhook

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

Terminal window
PATCH /webhooks/:id
Content-Type: application/json
{
"events": ["device.enrolled", "device.offline", "alert.triggered"],
"status": "paused"
}
Status ValueDescription
activeWebhook is enabled and will receive deliveries
pausedWebhook is temporarily disabled; no deliveries are sent
failedWebhook has been marked as failed (can be set manually or by the system)

Deleting a Webhook

Terminal window
DELETE /webhooks/:id

URL Safety Validation

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:

  • 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

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.


API Reference

MethodPathDescription
GET/webhooksList webhooks for the organization (paginated, filterable by status and orgId)
POST/webhooksCreate a new webhook
GET/webhooks/:idGet webhook details including delivery statistics
PATCH/webhooks/:idUpdate webhook name, URL, secret, events, headers, or status
DELETE/webhooks/:idDelete a webhook and all its delivery records
GET/webhooks/:id/deliveriesList delivery history (paginated, filterable by status)
POST/webhooks/:id/testSend a test delivery
POST/webhooks/:id/retry/:deliveryIdRetry a failed delivery

All webhook routes require authentication via the authMiddleware and are scoped to organization, partner, or system roles.


Database Schema

webhooks

ColumnTypeDescription
idUUIDPrimary key
org_idUUIDOrganization this webhook belongs to
namevarchar(255)Display name
urltextDestination URL (must be HTTPS)
secrettextEncrypted HMAC signing secret (AES-256-GCM)
eventstext[]Array of subscribed event type strings
headersJSONBCustom headers sent with each delivery
statusenumactive, disabled, or error
retry_policyJSONBOptional custom retry policy override
success_countintegerCumulative successful deliveries
failure_countintegerCumulative failed deliveries
last_delivery_attimestampTimestamp of the most recent delivery attempt
last_success_attimestampTimestamp of the most recent successful delivery
created_byUUIDUser who created the webhook
created_attimestampCreation timestamp
updated_attimestampLast modification timestamp

webhook_deliveries

ColumnTypeDescription
idUUIDPrimary key
webhook_idUUIDForeign key to webhooks
event_typevarchar(100)The event type that triggered this delivery
event_idvarchar(100)The unique event ID
payloadJSONBThe full event payload sent to the endpoint
statusenumpending, delivered, failed, or retrying
attemptsintegerNumber of delivery attempts made
next_retry_attimestampScheduled time for the next retry (if retrying)
response_statusintegerHTTP response status code from the endpoint
response_bodytextResponse body (truncated to 1,000 characters)
response_time_msintegerRound-trip time in milliseconds
error_messagetextError description if the delivery failed
created_attimestampWhen the delivery record was created
delivered_attimestampWhen the delivery was successfully completed

Troubleshooting

“Invalid webhook URL” error when creating a webhook. The URL must use HTTPS and must not resolve to a private, loopback, or link-local IP address. Hostnames like localhost, *.localhost, and *.local are also 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 are stuck in retrying status. The worker retries failed deliveries using exponential backoff. Check the nextAttemptAt field on the delivery to see when the next attempt is scheduled. If retries have been exhausted (default: 5 attempts), the delivery moves to failed and the job enters the dead letter queue.

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. 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.

“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, retrying, or already delivered cannot be retried through this endpoint.

“Organization context required” error. Webhook routes require an organization scope. If you are authenticated with a partner or system scope, pass the orgId query parameter (for list) or include it in the request body (for create) to specify which organization’s webhooks you are managing.

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.