Webhooks
Webhooks deliver real-time event notifications to your endpoints. HatiData signs every webhook payload with HMAC-SHA256 for verification.
List Webhooks
GET /v1/organizations/{org_id}/webhooks
Lists all webhooks for the organization. Requires Owner or Admin role.
Request:
curl https://api.hatidata.com/v1/organizations/org_a1b2c3d4/webhooks \
-H "Authorization: Bearer <jwt>"
Response 200 OK:
{
"data": [
{
"webhook_id": "wh_a1b2c3",
"url": "https://hooks.acme.com/hatidata",
"description": "Main notification endpoint",
"events": ["key.rotated", "key.expiring", "billing.warning", "policy.changed"],
"status": "active",
"last_delivered_at": "2026-02-16T09:00:00Z",
"last_status_code": 200,
"failure_count": 0,
"created_at": "2026-01-15T10:00:00Z"
}
],
"pagination": {
"cursor": null,
"has_more": false,
"total": 1
}
}
Create Webhook
POST /v1/organizations/{org_id}/webhooks
Creates a new webhook endpoint. HatiData generates a signing secret for HMAC-SHA256 verification. Requires Owner or Admin role.
Request:
curl -X POST https://api.hatidata.com/v1/organizations/org_a1b2c3d4/webhooks \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.acme.com/hatidata/security",
"description": "Security event notifications",
"events": [
"key.rotated",
"key.expiring",
"key.revoked",
"user.invited",
"user.removed",
"user.role_changed",
"policy.created",
"policy.updated",
"policy.deleted",
"jit.requested",
"jit.approved",
"jit.denied"
]
}'
Response 201 Created:
{
"webhook_id": "wh_d4e5f6",
"url": "https://hooks.acme.com/hatidata/security",
"description": "Security event notifications",
"events": [
"key.rotated",
"key.expiring",
"key.revoked",
"user.invited",
"user.removed",
"user.role_changed",
"policy.created",
"policy.updated",
"policy.deleted",
"jit.requested",
"jit.approved",
"jit.denied"
],
"signing_secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0",
"status": "active",
"created_at": "2026-02-16T10:00:00Z"
}
The signing_secret is shown only once in the creation response. Store it securely to verify incoming webhook payloads.
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS endpoint URL |
description | string | No | Human-readable description |
events | string[] | Yes | Event types to deliver |
Available Event Types
| Category | Events |
|---|---|
| API Keys | key.created, key.rotated, key.expiring, key.revoked |
| Users | user.invited, user.accepted, user.removed, user.role_changed, user.suspended |
| Policies | policy.created, policy.updated, policy.deleted |
| Billing | billing.warning, billing.critical, billing.quota_exceeded, billing.period_reset |
| JIT Access | jit.requested, jit.approved, jit.denied, jit.revoked, jit.expired |
| Environments | environment.created, environment.promoted, environment.suspended |
| Security | auth.failed, auth.mfa_enrolled, sso.configured |
Update Webhook
PUT /v1/organizations/{org_id}/webhooks/{webhook_id}
Updates webhook URL, description, or event subscriptions. Requires Owner or Admin role.
Request:
curl -X PUT https://api.hatidata.com/v1/organizations/org_a1b2c3d4/webhooks/wh_a1b2c3 \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.acme.com/hatidata/v2",
"events": ["key.rotated", "key.expiring", "billing.warning", "billing.critical"]
}'
Response 200 OK:
{
"webhook_id": "wh_a1b2c3",
"url": "https://hooks.acme.com/hatidata/v2",
"description": "Main notification endpoint",
"events": ["key.rotated", "key.expiring", "billing.warning", "billing.critical"],
"status": "active",
"updated_at": "2026-02-16T11:00:00Z"
}
Delete Webhook
DELETE /v1/organizations/{org_id}/webhooks/{webhook_id}
Deletes a webhook. No further events are delivered. Requires Owner or Admin role.
Request:
curl -X DELETE https://api.hatidata.com/v1/organizations/org_a1b2c3d4/webhooks/wh_d4e5f6 \
-H "Authorization: Bearer <jwt>"
Response 200 OK:
{
"webhook_id": "wh_d4e5f6",
"deleted": true,
"deleted_at": "2026-02-16T12:00:00Z"
}
Test Webhook
POST /v1/organizations/{org_id}/webhooks/{webhook_id}/test
Sends a test event to the webhook endpoint. Useful for verifying connectivity and signature validation. Requires Owner or Admin role.
Request:
curl -X POST https://api.hatidata.com/v1/organizations/org_a1b2c3d4/webhooks/wh_a1b2c3/test \
-H "Authorization: Bearer <jwt>"
Response 200 OK:
{
"webhook_id": "wh_a1b2c3",
"test_event_id": "evt_test_j0k1l2",
"delivered": true,
"status_code": 200,
"response_time_ms": 145,
"delivered_at": "2026-02-16T10:00:00Z"
}
Webhook Payload Format
All webhook deliveries use the following envelope:
{
"event_id": "evt_m3n4o5p6",
"event_type": "key.rotated",
"org_id": "org_a1b2c3d4",
"timestamp": "2026-02-16T10:00:00Z",
"data": {
"key_id": "key_a1b2c3",
"name": "analytics-dashboard",
"old_key_expires_at": "2026-02-19T10:00:00Z",
"rotated_by": "usr_x1y2z3"
}
}
Signature Verification
Every webhook includes an X-HatiData-Signature header containing an HMAC-SHA256 signature of the raw request body:
X-HatiData-Signature: sha256=a1b2c3d4e5f6...
Verify the signature in your handler:
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
import { createHmac } from 'crypto';
function verifyWebhook(payload: Buffer, signature: string, secret: string): boolean {
const expected = createHmac('sha256', secret).update(payload).digest('hex');
return signature === `sha256=${expected}`;
}
Retry Policy
Failed deliveries (non-2xx status codes) are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 12 hours |
After 5 failed retries, the webhook is marked as failed and no further deliveries are attempted until the endpoint is updated or tested successfully.
Error Responses
| Status | Code | Description |
|---|---|---|
400 | VALIDATION_ERROR | Invalid URL or event types |
401 | UNAUTHORIZED | Missing or invalid authentication |
403 | FORBIDDEN | Insufficient role (must be Owner or Admin) |
404 | NOT_FOUND | Webhook or organization not found |
422 | DELIVERY_FAILED | Test delivery failed (non-2xx response from endpoint) |