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:
| Event | Description |
|---|---|
danger | A check returned DANGER level (score 7-10) |
caution | A check returned CAUTION level (score 3-6) |
safe | A check returned SAFE level (score 0-2) |
all | All 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:
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS or HTTP endpoint to receive webhooks |
events | string[] | No | Event types to subscribe to. Default: ["danger"] |
secret | string | No | HMAC-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"
]
}| Field | Type | Description |
|---|---|---|
event | string | Event type (danger, caution, safe, or test) |
timestamp | string | ISO 8601 timestamp |
action_type | string | One of 18 action types (url, command, text, etc.) |
value_hash | string | SHA-256 hash of the checked value (privacy-preserving) |
score | number | Composite risk score (0-10) |
level | string | Risk level: SAFE, CAUTION, or DANGER |
reasons | string[] | Human-readable threat descriptions |
Headers included:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Surfinguard-Event | Event type (e.g., danger) |
X-Surfinguard-Signature | HMAC-SHA256 signature (only if secret is configured) |
User-Agent | Surfinguard-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', 200Failure Handling
Webhooks track delivery failures automatically:
- On success (2xx response): failure count resets to 0,
last_triggered_atis 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:
- Your app calls
/v2/check/commandwith a dangerous command - The API scores it → DANGER (score 9)
- The API returns the CheckResult to your app immediately
- 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
- Your webhook endpoint receives the notification
Webhook delivery never blocks the API response — it runs asynchronously via waitUntil().