Webhooks

Webhooks allow you to receive real-time HTTP notifications when events occur in your fleet. Perfect for alerting, automation, and integrating with external systems.

Security: All webhook payloads are signed with HMAC-SHA256. Always verify signatures in production.

Supported Events

Event Description When it fires
host.enrolled New host joins fleet First time a Shadow agent connects with your org token
host.online Host comes online Agent reconnects after being offline
host.offline Host goes offline Agent disconnects or stops responding
query.completed Query finishes A distributed query completes across all targeted hosts

Creating a Webhook

Create a webhook endpoint via the API:

curl -X POST https://hyprwatch.cloud/api/v1/webhooks \
-H "Authorization: Bearer hw_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhook",
"events": ["host.enrolled", "host.online", "host.offline"]
}'

Response:

{
"id": "wh_abc123",
"url": "https://your-app.com/webhook",
"events": ["host.enrolled", "host.online", "host.offline"],
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxx",
"active": true,
"created_at": "2024-01-15T10:30:00Z"
}
Save your secret! The webhook secret is only shown once. Store it securely for signature verification.

Webhook Payload

When an event occurs, we send a POST request to your URL with a JSON payload:

{
"id": "evt_xyz789",
"event": "host.online",
"timestamp": "2024-01-15T10:35:00Z",
"data": {
"host_id": "h_abc123",
"hostname": "server-01",
"platform": "linux",
"os_version": "Ubuntu 22.04",
"ip_address": "192.168.1.100"
}
}

Signature Verification

Every webhook request includes a signature header for verification:

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

Verify the signature by computing HMAC-SHA256 of the raw request body using your webhook secret:

Python Example

import hmac
import hashlib

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
    secret.encode(),
    payload,
    hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)

# In your webhook handler:
signature = request.headers.get("X-Hyprwatch-Signature")
if not verify_signature(request.body, signature, WEBHOOK_SECRET):
return Response(status=401)

Node.js Example

const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}

// Express middleware
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-hyprwatch-signature'];
if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process the webhook...
});

Example Payloads

host.enrolled

{
"id": "evt_001",
"event": "host.enrolled",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"host_id": "h_abc123",
"hostname": "new-server",
"platform": "linux",
"os_version": "Ubuntu 22.04 LTS",
"osquery_version": "5.10.2",
"shadow_version": "0.1.0"
}
}

host.online / host.offline

{
"id": "evt_002",
"event": "host.online",
"timestamp": "2024-01-15T10:35:00Z",
"data": {
"host_id": "h_abc123",
"hostname": "server-01",
"platform": "linux",
"last_seen": "2024-01-15T10:35:00Z",
"previous_state": "offline",
"offline_duration_seconds": 3600
}
}

query.completed

Fired when a scheduled query runs on a host. Each host sends its own webhook with results.

{
"id": "evt_003",
"event": "query.completed",
"timestamp": "2024-01-15T10:40:00Z",
"data": {
"query_id": "sq_abc123",
"query_name": "unauthorized_ssh_keys",
"sql": "SELECT * FROM authorized_keys WHERE ...",
"result_id": "r_xyz789",
"host_id": "h_def456",
"hostname": "prod-server-01",
"platform": "linux",
"row_count": 2,
"rows": [
  {"key": "ssh-rsa AAAA...", "key_file": "/root/.ssh/authorized_keys"},
  {"key": "ssh-rsa BBBB...", "key_file": "/root/.ssh/authorized_keys"}
],
"rows_truncated": false
}
}
Results included: Up to 100 rows are included directly in the webhook payload. For queries returning more rows, rows_truncated will be true.

Managing Webhooks

GET /webhooks

List all webhooks for your organization.

GET /webhooks/:id

Get details about a specific webhook, including delivery history.

PATCH /webhooks/:id

Update webhook URL, events, or active status.

DELETE /webhooks/:id

Delete a webhook.

POST /webhooks/:id/rotate-secret

Generate a new webhook secret. The old secret is invalidated immediately.

Retry Policy

If your endpoint returns a non-2xx status code or times out, we retry with exponential backoff:

  • Attempt 1: Immediate
  • Attempt 2: 1 minute delay
  • Attempt 3: 5 minutes delay
  • Attempt 4: 30 minutes delay
  • Attempt 5: 2 hours delay (final)

After 5 failed attempts, the webhook is marked as failed and disabled. You can re-enable it from the dashboard or API.

Integration Examples

Slack Notification

import requests

def handle_webhook(event):
if event["event"] == "host.offline":
    host = event["data"]
    requests.post(SLACK_WEBHOOK_URL, json={
        "text": f":warning: Host *{host['hostname']}* went offline",
        "attachments": [{
            "color": "warning",
            "fields": [
                {"title": "Platform", "value": host["platform"], "short": True},
                {"title": "Last Seen", "value": host["last_seen"], "short": True}
            ]
        }]
    })

PagerDuty Alert

import requests

def handle_webhook(event):
if event["event"] == "host.offline":
    host = event["data"]
    # Only alert for critical hosts
    if "production" in host.get("labels", []):
        requests.post("https://events.pagerduty.com/v2/enqueue", json={
            "routing_key": PAGERDUTY_KEY,
            "event_action": "trigger",
            "payload": {
                "summary": f"Production host {host['hostname']} offline",
                "severity": "critical",
                "source": "hyprwatch",
                "custom_details": host
            }
        })

Best Practices

Always verify signatures

Never process webhook payloads without verifying the HMAC signature. This prevents replay attacks and payload tampering.

Respond quickly

Return a 2xx response within 30 seconds. Process the payload asynchronously if needed.

Handle duplicates

Use the event id field to deduplicate. Retries may deliver the same event multiple times.

Use HTTPS

Webhook URLs must use HTTPS. We do not deliver to HTTP endpoints in production.