Skip to main content

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"
}
Save the Signing Secret

The signing_secret is shown only once in the creation response. Store it securely to verify incoming webhook payloads.

FieldTypeRequiredDescription
urlstringYesHTTPS endpoint URL
descriptionstringNoHuman-readable description
eventsstring[]YesEvent types to deliver

Available Event Types

CategoryEvents
API Keyskey.created, key.rotated, key.expiring, key.revoked
Usersuser.invited, user.accepted, user.removed, user.role_changed, user.suspended
Policiespolicy.created, policy.updated, policy.deleted
Billingbilling.warning, billing.critical, billing.quota_exceeded, billing.period_reset
JIT Accessjit.requested, jit.approved, jit.denied, jit.revoked, jit.expired
Environmentsenvironment.created, environment.promoted, environment.suspended
Securityauth.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:

AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry12 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

StatusCodeDescription
400VALIDATION_ERRORInvalid URL or event types
401UNAUTHORIZEDMissing or invalid authentication
403FORBIDDENInsufficient role (must be Owner or Admin)
404NOT_FOUNDWebhook or organization not found
422DELIVERY_FAILEDTest delivery failed (non-2xx response from endpoint)

Stay in the loop

Product updates, engineering deep-dives, and agent-native insights. No spam.