Webhook Event Reference¶
ZKProva sends webhook notifications when verification events occur. Configure webhooks to receive real-time updates in your application.
Event Types¶
| Event | Trigger |
|---|---|
verification.completed |
ZKP proof verification succeeded (all claims verified) |
verification.failed |
ZKP proof verification failed (one or more claims failed) |
credential.revoked |
A member revoked one of their credentials |
Payload Schema¶
All webhook payloads are JSON with this envelope:
{
"event": "<event_type>",
"timestamp": "2026-02-28T14:30:00Z",
"delivery_id": "550e8400-e29b-41d4-a716-446655440000",
"data": { ... }
}
verification.completed¶
{
"event": "verification.completed",
"member_did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"claims_verified": {
"income_range": true,
"membership_status": true
},
"all_verified": true,
"verification_id": "550e8400-e29b-41d4-a716-446655440000",
"failed_step": null,
"claim_results": [
{"claim_type": "income_range", "passed": true, "reason": "Proof verified"},
{"claim_type": "membership_status", "passed": true, "reason": "Proof verified"}
]
}
verification.failed¶
{
"event": "verification.failed",
"member_did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"claims_verified": {
"income_range": false
},
"all_verified": false,
"verification_id": "",
"failed_step": "ZKP_VERIFY",
"claim_results": [
{"claim_type": "income_range", "passed": false, "reason": "Groth16 proof invalid"}
]
}
credential.revoked¶
{
"event": "credential.revoked",
"credential_id": "550e8400-e29b-41d4-a716-446655440000",
"claim_type": "income_range",
"member_did": "did:key:z6Mk...",
"revoked_at": "2026-02-28T14:30:00Z"
}
Signature Verification¶
Every webhook request includes an HMAC-SHA256 signature in the X-ZKProva-Signature header:
Verify the signature by computing HMAC-SHA256 of the raw request body using your webhook secret.
Node.js¶
const crypto = require('crypto');
function verifyWebhookSignature(body, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express middleware
app.post('/webhooks/zkprova', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-zkprova-signature'];
if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log(`Received ${event.event}`);
res.sendStatus(200);
});
Python¶
import hashlib
import hmac
def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
Retry Policy¶
Failed deliveries (non-2xx response or timeout) are retried automatically:
| Attempt | Delay | Cumulative |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 30 seconds | 30s |
| 3 | 5 minutes | ~5.5 min |
| 4 | 1 hour | ~1 hr 5 min |
After 4 failed attempts, the delivery moves to the dead-letter queue.
Dead-Letter Queue¶
Failed webhooks that exhaust all retries are held in a dead-letter queue. Admins can replay them:
curl -X POST https://api.zkprova.com/api/v1/admin/webhooks/dlq/replay \
-H "Authorization: Bearer <admin_jwt>" \
-H "Content-Type: application/json" \
-d '{"delivery_ids": ["<delivery_id>"]}'
View dead-letter entries:
Webhook CRUD¶
Register a Webhook¶
curl -X POST https://api.zkprova.com/api/v1/webhooks \
-H "X-API-Key: zkp_live_abc123" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/zkprova",
"events": ["verification.completed", "verification.failed"]
}'