Webhooks

Webhooks allow you to receive real-time notifications when Surfinguard detects threats. Instead of polling the API, your server receives an HTTP POST request whenever a configured event occurs.

All webhook endpoints require authentication via API key.


Event Types

Webhooks support filtering by risk level:

EventDescription
dangerA check returned DANGER level (score 7-10)
cautionA check returned CAUTION level (score 3-6)
safeA check returned SAFE level (score 0-2)
allAll check results regardless of level

By default, new webhooks subscribe to ["danger"] only.


Create a Webhook

curl -X POST https://v2.surfinguard.com/v2/webhooks \
  -H "Authorization: Bearer sg_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhooks/surfinguard",
    "events": ["danger"],
    "secret": "your_webhook_secret"
  }'

Response (201):

{
  "id": "a1b2c3d4-e5f6-...",
  "url": "https://your-server.com/webhooks/surfinguard",
  "events": ["danger"],
  "active": true,
  "created_at": "2026-02-23 10:30:00"
}

Parameters:

FieldTypeRequiredDescription
urlstringYesHTTPS or HTTP endpoint to receive webhooks
eventsstring[]NoEvent types to subscribe to. Default: ["danger"]
secretstringNoHMAC-SHA256 signing secret for payload verification

List Webhooks

curl https://v2.surfinguard.com/v2/webhooks \
  -H "Authorization: Bearer sg_live_..."

Response:

{
  "webhooks": [
    {
      "id": "a1b2c3d4-...",
      "url": "https://your-server.com/webhooks/surfinguard",
      "events": ["danger"],
      "active": true,
      "created_at": "2026-02-23 10:30:00",
      "last_triggered_at": "2026-02-23 12:15:00",
      "failure_count": 0
    }
  ]
}

Update a Webhook

curl -X PUT https://v2.surfinguard.com/v2/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer sg_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["danger", "caution"],
    "active": true
  }'

You can update url, events, secret, and active fields. Only include the fields you want to change.


Delete a Webhook

curl -X DELETE https://v2.surfinguard.com/v2/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer sg_live_..."

Test a Webhook

Send a test payload to verify your endpoint is working:

curl -X POST https://v2.surfinguard.com/v2/webhooks/WEBHOOK_ID/test \
  -H "Authorization: Bearer sg_live_..."

Response:

{
  "sent": true,
  "status": 200
}

Webhook Payload

When a check triggers a webhook, your endpoint receives a POST request with this payload:

{
  "event": "danger",
  "timestamp": "2026-02-23T10:30:00.000Z",
  "action_type": "command",
  "value_hash": "e3b0c44298fc1c149afbf4c8996fb924....",
  "score": 9,
  "level": "DANGER",
  "reasons": [
    "Destructive command: rm with recursive force and root target",
    "No-preserve-root flag detected"
  ]
}
FieldTypeDescription
eventstringEvent type (danger, caution, safe, or test)
timestampstringISO 8601 timestamp
action_typestringOne of 18 action types (url, command, text, etc.)
value_hashstringSHA-256 hash of the checked value (privacy-preserving)
scorenumberComposite risk score (0-10)
levelstringRisk level: SAFE, CAUTION, or DANGER
reasonsstring[]Human-readable threat descriptions

Headers included:

HeaderDescription
Content-Typeapplication/json
X-Surfinguard-EventEvent type (e.g., danger)
X-Surfinguard-SignatureHMAC-SHA256 signature (only if secret is configured)
User-AgentSurfinguard-Webhook/1.0

HMAC-SHA256 Signature Verification

If you provide a secret when creating the webhook, every delivery includes an X-Surfinguard-Signature header. Verify it on your server to ensure the request is authentic.

JavaScript / Node.js

import crypto from 'crypto';
 
function verifyWebhook(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return `sha256=${expected}` === signature;
}
 
// Express example
app.post('/webhooks/surfinguard', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-surfinguard-signature'];
  if (!verifyWebhook(req.body, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
 
  const event = JSON.parse(req.body);
  console.log(`Received ${event.event}: score ${event.score}, type ${event.action_type}`);
  res.status(200).send('OK');
});

Python

import hmac
import hashlib
 
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return f"sha256={expected}" == signature
 
# Flask example
@app.route('/webhooks/surfinguard', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Surfinguard-Signature', '')
    if not verify_webhook(request.data, signature, os.environ['WEBHOOK_SECRET']):
        return 'Invalid signature', 401
 
    event = request.json
    print(f"Received {event['event']}: score {event['score']}, type {event['action_type']}")
    return 'OK', 200

Failure Handling

Webhooks track delivery failures automatically:

  • On success (2xx response): failure count resets to 0, last_triggered_at is updated
  • On failure (non-2xx or network error): failure count increments by 1
  • After 10 consecutive failures: the webhook is automatically deactivated (active: false)

To re-enable a deactivated webhook:

curl -X PUT https://v2.surfinguard.com/v2/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer sg_live_..." \
  -d '{"active": true}'

How It Works

Webhooks fire after every /v2/check response when the result matches your subscribed events:

  1. Your app calls /v2/check/command with a dangerous command
  2. The API scores it → DANGER (score 9)
  3. The API returns the CheckResult to your app immediately
  4. In the background (fire-and-forget), the API:
    • Queries active webhooks for your API key
    • Filters by subscribed event type
    • Signs the payload with HMAC (if secret configured)
    • POSTs to each matching webhook URL
  5. Your webhook endpoint receives the notification

Webhook delivery never blocks the API response — it runs asynchronously via waitUntil().