Skip to content

Webhooks

Webhooks let VisibilityIQ push a notification to your server when an audit completes, rather than requiring you to poll the API. This is the recommended approach for production integrations.

EventTrigger
audit.completedA crawl completed successfully
audit.failedA crawl failed after all retries
connector.sync.completedA connector sync completed
connector.sync.failedA connector sync failed

Go to Account → Webhooks → Add endpoint. Enter your HTTPS URL and select which events to subscribe to. VisibilityIQ will send a test event immediately — verify you receive it before saving.

Webhook URLs must be https:// — plain HTTP is not accepted.

All webhook events share a common envelope:

{
"id": "evt_abc123def456",
"event": "audit.completed",
"projectId": "proj_xyz789",
"timestamp": "2026-06-15T14:38:00Z",
"data": { /* event-specific payload */ }
}
{
"id": "evt_abc123",
"event": "audit.completed",
"projectId": "proj_xyz789",
"timestamp": "2026-06-15T14:38:00Z",
"data": {
"auditId": "audit_qqq111",
"domain": "example.com",
"pagesCrawled": 847,
"findingCounts": {
"critical": 2,
"high": 14,
"medium": 47,
"low": 103,
"info": 8
},
"durationSeconds": 892
}
}

Every webhook request includes a X-VisibilityIQ-Signature header — an HMAC-SHA256 signature of the request body using your webhook secret.

Verify signatures before processing any webhook payload. This prevents attackers from sending fake events to your endpoint.

import { createHmac } from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expected = createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
const expectedBuf = Buffer.from(`sha256=${expected}`);
const sigBuf = Buffer.from(signature);
if (expectedBuf.length !== sigBuf.length) return false;
return timingSafeEqual(expectedBuf, sigBuf);
}
// In your handler:
app.post('/webhooks/visibilityiq', (req, res) => {
const signature = req.headers['x-visibilityiq-signature'] as string;
const rawBody = req.rawBody; // important: use raw body, not parsed JSON
if (!verifyWebhookSignature(rawBody, signature, process.env.WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(rawBody);
// process event...
res.status(200).send('ok');
});

If your endpoint returns a non-2xx response or doesn’t respond within 30 seconds, VisibilityIQ retries the event with exponential backoff:

AttemptDelay
1 (initial)Immediate
230 seconds
35 minutes
41 hour
56 hours

After 5 failed attempts, the event is marked as failed and delivery stops. You can view failed events and manually retry them under Account → Webhooks → Event log.

Your handler should be idempotent — the same event may be delivered more than once (network issues can cause retries even when your endpoint returned 200). Use the id field to deduplicate events.

const seen = new Set<string>();
app.post('/webhooks/visibilityiq', (req, res) => {
const event = req.body;
if (seen.has(event.id)) {
return res.status(200).send('already processed');
}
seen.add(event.id);
// process...
});

In production, use Redis or a database instead of an in-memory Set for the deduplication store.