Webhooks
WPSentry sends HTTP POST requests to your configured URLs when events occur in your account. Use webhooks to get notified in real-time when scans finish, monitored sites go down, or recover from downtime — without polling the API.
Webhooks are available on Pro (up to 3 webhooks) and Enterprise (up to 10 webhooks) plans.
Configure webhooks in your Account Settings.
Setup
Follow these steps to configure your first webhook:
- Go to Account > Webhooks tab.
- Click "Create Webhook".
- Enter your HTTPS endpoint URL.
- Select which events to subscribe to.
- Copy the signing secret (shown only once).
Note: Your endpoint URL must use HTTPS. The signing secret is used to verify that incoming requests are genuinely from WPSentry — store it securely.
Event Types
Subscribe to one or more of the following events:
| Parameter | Type | Description |
|---|---|---|
scan.completed | string | Fired when a security scan finishes. Includes score, issue counts, and scan ID. |
site.down | string | Fired when a monitored site transitions from up to down. |
site.up | string | Fired when a monitored site recovers from downtime. |
webhook.test | string | Fired when you click the Test button. Used for verifying your endpoint. |
Payload Format
All webhook payloads follow the same envelope structure:
{
"event": "scan.completed",
"timestamp": "2025-01-15T12:00:00.000Z",
"data": { ... }
}scan.completed
{
"event": "scan.completed",
"timestamp": "2025-01-15T12:00:00.000Z",
"data": {
"scanId": "clx1abc2d0001...",
"url": "https://example.com",
"domain": "example.com",
"score": 72,
"issueCount": 8,
"criticalCount": 1,
"highCount": 3,
"pluginCount": 12,
"vulnerablePluginCount": 2
}
}Data Fields
| Parameter | Type | Description |
|---|---|---|
scanId | string | Unique scan identifier |
url | string | Full URL that was scanned |
domain | string | Domain name |
score | number | Security score (0-100) |
issueCount | number | Total number of issues found |
criticalCount | number | Number of critical issues |
highCount | number | Number of high severity issues |
pluginCount | number | Number of detected plugins |
vulnerablePluginCount | number | Number of plugins with known vulnerabilities |
site.down
{
"event": "site.down",
"timestamp": "2025-01-15T12:00:00.000Z",
"data": {
"siteId": "clx1abc2d0002...",
"url": "https://example.com",
"statusCode": 503,
"errorMsg": "HTTP 503",
"consecutiveDown": 1
}
}Data Fields
| Parameter | Type | Description |
|---|---|---|
siteId | string | Monitored site identifier |
url | string | Site URL |
statusCode | number|null | HTTP status code (null if connection failed) |
errorMsg | string|null | Error message |
consecutiveDown | number | Number of consecutive failed checks |
site.up
{
"event": "site.up",
"timestamp": "2025-01-15T12:00:00.000Z",
"data": {
"siteId": "clx1abc2d0002...",
"url": "https://example.com",
"statusCode": 200,
"responseMs": 342,
"downDurationSeconds": 600
}
}Data Fields
| Parameter | Type | Description |
|---|---|---|
siteId | string | Monitored site identifier |
url | string | Site URL |
statusCode | number | HTTP status code |
responseMs | number | Response time in milliseconds |
downDurationSeconds | number | How long the site was down in seconds |
Verifying Signatures
Every webhook delivery includes an X-WPSentry-Signature header containing an HMAC-SHA256 signature of the raw request body, using your webhook's signing secret.
Headers
| Parameter | Type | Description |
|---|---|---|
X-WPSentry-Signature | string | sha256=<hex digest> HMAC-SHA256 signature |
X-WPSentry-Event | string | Event type (e.g. scan.completed) |
X-WPSentry-Delivery | string | Unique delivery ID |
Content-Type | string | Always application/json |
User-Agent | string | WPSentry-Webhook/1.0 |
Node.js Verification
const crypto = require("crypto");
function verifyWebhook(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
return signature === `sha256=${expected}`;
}
// Express.js example
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-wpsentry-signature"];
if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(req.body);
console.log("Event:", payload.event, payload.data);
res.status(200).send("OK");
});Python Verification
import hmac
import hashlib
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
body,
hashlib.sha256
).hexdigest()
return signature == f"sha256={expected}"PHP Verification
function verifyWebhook(string $body, string $signature, string $secret): bool {
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
return hash_equals($expected, $signature);
}Retry Policy
If your endpoint returns a non-2xx status code or is unreachable, WPSentry retries delivery with exponential backoff:
| Attempt | Delay |
|---|---|
| Attempt 1 | Immediate |
| Attempt 2 | After 1 minute |
| Attempt 3 | After 5 minutes |
| Attempt 4 | After 30 minutes |
| Attempt 5 | After 2 hours |
After 5 failed attempts, the delivery is marked as permanently failed.
If a webhook accumulates 10 consecutive delivery failures (across any deliveries), it is automatically disabled. You can re-enable it from the dashboard.
Successful deliveries reset the failure counter.
API Management
You can also manage webhooks programmatically via the REST API. All endpoints require authentication via session cookie.
/api/user/webhooks— List all webhookscurl -X GET https://yourdomain.com/api/user/webhooks \ -H "Cookie: wp_scanner_session=YOUR_SESSION"
/api/user/webhooks— Create a new webhook| Parameter | Type | Description |
|---|---|---|
name | string | Display name for the webhook |
url | string | HTTPS endpoint URL |
events | string[] | Array of event types to subscribe to |
curl -X POST https://yourdomain.com/api/user/webhooks \
-H "Content-Type: application/json" \
-H "Cookie: wp_scanner_session=YOUR_SESSION" \
-d '{"name":"My Webhook","url":"https://example.com/hook","events":["scan.completed"]}'/api/user/webhooks/:id— Update a webhookcurl -X PATCH https://yourdomain.com/api/user/webhooks/WEBHOOK_ID \
-H "Content-Type: application/json" \
-H "Cookie: wp_scanner_session=YOUR_SESSION" \
-d '{"isActive":false}'/api/user/webhooks/:id— Delete a webhookcurl -X DELETE https://yourdomain.com/api/user/webhooks/WEBHOOK_ID \ -H "Cookie: wp_scanner_session=YOUR_SESSION"
/api/user/webhooks/:id/test— Send a test deliverycurl -X POST https://yourdomain.com/api/user/webhooks/WEBHOOK_ID/test \ -H "Cookie: wp_scanner_session=YOUR_SESSION"
/api/user/webhooks/:id/deliveries— View delivery historycurl -X GET https://yourdomain.com/api/user/webhooks/WEBHOOK_ID/deliveries \ -H "Cookie: wp_scanner_session=YOUR_SESSION"
Integration Examples
Slack
Forward WPSentry events to a Slack channel via Incoming Webhooks.
// Slack Incoming Webhook integration
// Set your Slack webhook URL as the WPSentry webhook endpoint
// Or use a proxy that formats the message:
app.post("/wpsentry-to-slack", express.json(), async (req, res) => {
const { event, data } = req.body;
let text;
if (event === "scan.completed") {
const emoji = data.score >= 80 ? ":white_check_mark:" : ":warning:";
text = `${emoji} Scan completed for *${data.domain}*\nScore: ${data.score}/100 | Issues: ${data.issueCount}`;
} else if (event === "site.down") {
text = `:red_circle: *${data.url}* is DOWN\nError: ${data.errorMsg}`;
} else if (event === "site.up") {
text = `:large_green_circle: *${data.url}* is back UP\nDowntime: ${Math.round(data.downDurationSeconds / 60)} minutes`;
}
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }),
});
res.status(200).send("OK");
});Discord
Forward WPSentry events to a Discord channel via Discord Webhooks.
// Discord webhook integration
app.post("/wpsentry-to-discord", express.json(), async (req, res) => {
const { event, data } = req.body;
let content;
if (event === "scan.completed") {
content = `**Scan Complete:** ${data.domain} — Score: ${data.score}/100 (${data.issueCount} issues)`;
} else if (event === "site.down") {
content = `**Site Down:** ${data.url} — ${data.errorMsg}`;
} else if (event === "site.up") {
content = `**Site Recovered:** ${data.url} — Down for ${Math.round(data.downDurationSeconds / 60)}m`;
}
await fetch(process.env.DISCORD_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
});
res.status(200).send("OK");
});