Webhooks and Slack notifications
Get every finished audit pushed to Slack, Zapier or your own endpoint, the moment it is ready.
Overview
Notifications are part of the Pro plan ($29/month). Add up to 10 destinations in Settings, Integrations, then choose which events each one receives. Every destination has a Send test button, so you can check it works before a real audit finishes.
Slack
- In your Slack workspace, create an app (or add the Incoming Webhooks app).
- Turn on incoming webhooks for the app and add it to the channel you want.
- Copy the webhook URL. It starts with
https://hooks.slack.com/. - Paste it into Settings, Integrations, Slack, Add, then press Send test.
Zapier
- Create a Zap and choose Webhooks by Zapier as the trigger, with the event Catch Hook.
- Copy the URL Zapier gives you.
- Paste it into Settings, Integrations, Webhooks, Add, then press Send test.
- Back in Zapier, find the test request and build the rest of the Zap from its fields.
Events
| Event | When it is sent |
|---|---|
| audit.completed | Audit complete. An audit finished and has a score. |
| audit.diff | Monitored site changed. Every scheduled re-audit of a monitored site that has an earlier audit to compare against, whether or not anything changed. |
| audit.critical | Critical issues found. A finished audit has at least one failing critical check. |
| report.ready | Report ready. A report you bought is ready to open. |
| fix.verified | Fix verified. An issue you marked as fixed was checked and is fixed. |
| audit.failed | Audit failed. An audit could not finish. |
Payload
Webhooks receive a JSON body like this one. Slack destinations get the same facts as a formatted message.
{
"id": "sample",
"event": "audit.completed",
"created_at": "2026-11-04T09:00:00.000Z",
"account": {
"id": "sample"
},
"site": {
"url": "https://example.com",
"host": "example.com"
},
"audit": {
"id": "sample",
"url": "https://auditzap.io/audits/sample",
"score": 78,
"previous_score": 74,
"delta": 4,
"pages_audited": 10,
"source": "dashboard",
"completed_at": "2026-11-04T09:00:00.000Z"
},
"top_issues": [
{
"check": "Missing meta description",
"category": "seo",
"status": "fail"
},
{
"check": "llms.txt present",
"category": "ai_visibility",
"status": "warn"
}
],
"summary": {
"issues": 6,
"passes": 34,
"critical_failures": 1
}
}Headers
| Header | Value |
|---|---|
| Content-Type | application/json |
| User-Agent | AuditZap-Webhooks/1 |
| X-AuditZap-Event | The event name, for example audit.completed. |
| X-AuditZap-Delivery | A unique id for this delivery. Retries reuse it, so you can ignore repeats. |
| X-AuditZap-Timestamp | When we signed the request, in unix seconds. |
| X-AuditZap-Signature | v1= followed by the hex HMAC-SHA256 of "<timestamp>.<body>", keyed with your signing secret. |
Verifying the signature
Each webhook destination has its own signing secret, shown in Settings. Compute v1= plus the hex HMAC-SHA256 of the timestamp, a full stop and the raw body, then compare it with the signature header in constant time. Reject requests whose timestamp is more than 5 minutes old.
Node:
import crypto from 'node:crypto'
// rawBody: the request body exactly as received, before any JSON parsing.
function verify(secret, headers, rawBody) {
const timestamp = headers['x-auditzap-timestamp']
const received = String(headers['x-auditzap-signature'] || '').replace(/^v1=/, '')
// Reject anything older than 5 minutes to stop replays.
if (Number(timestamp) + 300 < Date.now() / 1000) return false
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(received)
return a.length === b.length && crypto.timingSafeEqual(a, b)
}Python:
import hashlib, hmac, time
# raw_body: the request body exactly as received, decoded as UTF-8.
def verify(secret, headers, raw_body):
timestamp = headers["X-AuditZap-Timestamp"]
received = headers.get("X-AuditZap-Signature", "").removeprefix("v1=")
# Reject anything older than 5 minutes to stop replays.
if int(timestamp) + 300 < time.time():
return False
expected = hmac.new(secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received)Retries and failures
Only a 2xx response counts as delivered. We wait up to 10 seconds for a response and never follow redirects, so a 3xx counts as a failure. A failed delivery is tried 3 times in total, with an exponential backoff starting at 10 seconds.
After 10 failures in a row we pause the destination and email you. Fix the endpoint, then turn the destination back on in Settings.
Reporting abuse
If an AuditZap webhook is sending requests you did not ask for, tell us and we will stop it.