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.
Webhook events
Section titled “Webhook events”| Event | Trigger |
|---|---|
audit.completed | A crawl completed successfully |
audit.failed | A crawl failed after all retries |
connector.sync.completed | A connector sync completed |
connector.sync.failed | A connector sync failed |
Registering a webhook endpoint
Section titled “Registering a webhook endpoint”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.
Webhook payload
Section titled “Webhook payload”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 */ }}audit.completed payload
Section titled “audit.completed 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 }}Verifying webhook signatures
Section titled “Verifying webhook signatures”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');});Retry policy
Section titled “Retry policy”If your endpoint returns a non-2xx response or doesn’t respond within 30 seconds, VisibilityIQ retries the event with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 (initial) | Immediate |
| 2 | 30 seconds |
| 3 | 5 minutes |
| 4 | 1 hour |
| 5 | 6 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.
Idempotency
Section titled “Idempotency”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.