API reference
Webhooks
Webhooks let your server receive real-time notifications whenever a policy event occurs in your PolicifyAI workspace. No polling required.
Available events
policy.publishedA policy has been published to a live URL.
policy.updatedA policy has been edited or regenerated.
policy.deletedA policy has been permanently deleted.
consent.recordedA visitor recorded a cookie/consent choice.
dsar.submittedA data subject access request was submitted.
cookie_scan.completedA compliance scanner scan finished.
integration.connectedAn integration (e.g. WordPress, Shopify) was connected.
integration.disconnectedAn integration was disconnected.
Payload structure
All webhook deliveries send a JSON payload with this structure:
{
"event": "policy.published",
"webhook_id": "wh_abc123",
"delivery_id": "del_xyz789",
"created_at": "2025-01-01T12:00:00Z",
"data": {
"policy_id": "pol_abc123",
"policy_type": "privacy-policy",
"business_name": "Acme Ltd",
"domain": "https://acme.com",
"jurisdiction": "GB",
"language": "en",
"client_id": null,
"status": "ready",
"created_at": "2025-01-01T12:00:00Z",
"updated_at": "2025-01-01T12:01:30Z"
}
}policy.created event to any configured endpoint - separate from policy.published, which fires when a policy is published to a live URL. content_html is not included in webhook payloads to keep delivery sizes small; use the generate endpoint's response or list policies to fetch full content.Signature verification
Every webhook delivery includes an X-PolicifyAI-Signature header - an HMAC-SHA256 hex digest of the raw request body, keyed with your webhook secret. Always verify this signature before processing.
The same digest is also sent as X-PolicifyAI-Signature-256 in the prefixed sha256=<hex> form. Verify whichever you prefer - they cover identical bytes. Compare with a constant-time function (crypto.timingSafeEqual, hmac.compare_digest, hash_equals), never ===, and always hash the raw body rather than a re-serialised object.
Node.js
const crypto = require('crypto')
// Express middleware
app.post('/webhooks/policify', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-policifyai-signature']
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(req.body)
.digest('hex')
const ok = signature?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
if (!ok) {
return res.status(401).send('Invalid signature')
}
const event = JSON.parse(req.body)
// Process event...
res.sendStatus(200)
})Python
import hmac
import hashlib
def verify_signature(payload: bytes, secret: str, signature: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)PHP
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_POLICIFYAI_SIGNATURE'];
$expected = hash_hmac('sha256', $payload, getenv('WEBHOOK_SECRET'));
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}Retries
PolicifyAI retries failed deliveries (non-2xx responses or timeouts) up to 5 times with exponential backoff:
Attempt 1
Immediate
Attempt 2
30 s
Attempt 3
2 min
Attempt 4
10 min
Attempt 5
1 hour
After 5 failed attempts the webhook is automatically disabled and you receive an email. You can re-enable it from the API & Webhooks page. Any delivery can be manually replayed from the delivery history.
Response requirements
Your endpoint must return a 2xx HTTP status within 10 seconds. If you need to do heavy processing, respond immediately with 200 OK and process asynchronously. Do not return 3xx redirects - PolicifyAI does not follow them.
Testing webhooks
In the Agency Hub, open the webhook detail page and click Send test event. This sends a policy.published payload with synthetic data to your endpoint so you can verify your handler works without needing to generate a real policy.