Webhooks API
Webhook endpoints
Section titled “Webhook endpoints”| Method | Path | Description |
|---|---|---|
POST |
/api/v1/webhooks |
Create webhook |
GET |
/api/v1/webhooks |
List webhooks |
GET |
/api/v1/webhooks/:id |
Get webhook |
PATCH |
/api/v1/webhooks/:id |
Update webhook |
DELETE |
/api/v1/webhooks/:id |
Delete webhook |
POST |
/api/v1/webhooks/:id/test |
Test webhook delivery |
GET |
/api/v1/webhooks/:id/deliveries |
List deliveries for webhook |
Delivery endpoints (DLQ)
Section titled “Delivery endpoints (DLQ)”| Method | Path | Description |
|---|---|---|
GET |
/api/v1/webhooks/deliveries/stats |
Get delivery statistics |
GET |
/api/v1/webhooks/deliveries/failed |
List failed deliveries |
POST |
/api/v1/webhooks/deliveries/retry-all |
Retry all failed |
POST |
/api/v1/webhooks/deliveries/:deliveryId/retry |
Retry single delivery |
Other endpoints
Section titled “Other endpoints”| Method | Path | Description |
|---|---|---|
GET |
/api/v1/audit-logs |
List audit logs (with filters) |
GET |
/api/v1/audit-logs/:id |
Get single audit entry |
POST |
/api/v1/tenants/:id/consent |
Grant consent |
GET |
/api/v1/tenants/:id/consent |
List consent records |
DELETE |
/api/v1/tenants/:id/consent/:purpose |
Revoke consent |
POST |
/api/v1/maintenance/purge-expired |
Purge expired data |
POST |
/api/v1/maintenance/rotate-encryption-key |
Rotate encryption key |
Create webhook
Section titled “Create webhook”POST /api/v1/webhooksBody:
{ "url": "https://your-app.com/webhooks/stratum", "tenant_id": "uuid (optional, null for global)", "events": ["tenant.created", "config.updated"], "secret": "your-signing-secret"}| Field | Type | Required | Description |
|---|---|---|---|
url |
string | Yes | HTTPS endpoint for deliveries |
tenant_id |
UUID | No | Scope to a tenant (null for all events) |
events |
string[] | Yes | Event types to subscribe to |
secret |
string | No | HMAC-SHA256 signing secret (encrypted at rest) |
Supported events: tenant.created, tenant.updated, tenant.deleted, tenant.moved, tenant.activated, tenant.suspended, tenant.resumed, tenant.archived, config.updated, config.deleted, permission.created, permission.updated, permission.deleted
tenant.purged is deprecated. The API accepts a subscription to it, but a tenant purge never emits it. The next major version removes it.
Response: 201 Created. Returns the webhook object.
List webhooks
Section titled “List webhooks”GET /api/v1/webhooks?tenant_id=<uuid>Scoped keys automatically filter to their own tenant’s webhooks.
Response: 200 OK with an array of webhook objects.
Update webhook
Section titled “Update webhook”PATCH /api/v1/webhooks/:idBody (all optional):
{ "url": "https://new-endpoint.com/hook", "events": ["tenant.created", "tenant.updated"], "secret": "new-signing-secret"}Response: 200 OK. Returns the updated webhook.
Test webhook
Section titled “Test webhook”POST /api/v1/webhooks/:id/testSends a synthetic test event to the webhook URL and reports the response.
Response: 200 OK
{ "success": true, "response_code": 200}Delivery statistics
Section titled “Delivery statistics”GET /api/v1/webhooks/deliveries/statsResponse: 200 OK
{ "total": 150, "pending": 5, "success": 130, "failed": 15}List failed deliveries
Section titled “List failed deliveries”GET /api/v1/webhooks/deliveries/failed?limit=100Returns failed deliveries (dead-letter queue) with joined webhook and event data.
| Param | Type | Default | Description |
|---|---|---|---|
limit |
integer | 100 | Max results (1-500) |
Response: 200 OK with an array of failed delivery objects.
Retry all failed
Section titled “Retry all failed”POST /api/v1/webhooks/deliveries/retry-allResets all failed deliveries for retry (clears attempt counter, sets status to pending).
Response: 200 OK
{ "retried": 15}Retry single delivery
Section titled “Retry single delivery”POST /api/v1/webhooks/deliveries/:deliveryId/retryResponse: 200 OK with {"success": true}
Audit logs
Section titled “Audit logs”GET /api/v1/audit-logsQuery Parameters:
| Param | Type | Description |
|---|---|---|
tenant_id |
UUID | Filter by tenant |
action |
string | Filter by action (e.g., tenant.created) |
resource_type |
string | Filter by resource type |
actor_id |
string | Filter by actor |
from |
ISO 8601 | Start of date range |
to |
ISO 8601 | End of date range |
limit |
number | Max results, 1-100 (default: 50) |
cursor |
UUID | Cursor for keyset pagination |
Response: 200 OK with an array of audit entries.
Known secrets are redacted in audit entries: sensitive config values and webhook secrets are recorded as [REDACTED], and webhook URLs and region control_plane_url values are recorded as scheme://host/#fp=<fingerprint>. The fingerprint is the first 12 hex characters of the SHA-256 of the URL path, so entries for different endpoints can be told apart without recording the path; credentials, path, query string and fragment are dropped. A URL with an empty path is recorded as scheme://host/, and a URL without a host as [REDACTED].
Rotate encryption key
Section titled “Rotate encryption key”POST /api/v1/maintenance/rotate-encryption-keyRe-encrypts all sensitive config entries and webhook secrets from the old key to the new key. The rotation runs in batches of 100, and each batch commits in its own transaction. If a request fails partway, send it again with the same keys. Values that already decrypt with the new key stay unchanged.
Body:
{ "old_key": "current-encryption-key", "new_key": "new-encryption-key", "old_salt": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", "new_salt": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2"}| Field | Required | Meaning |
|---|---|---|
old_key |
Yes | The key that the existing values use. |
new_key |
Yes | The key that the rotation encrypts under. |
old_salt |
No | The HKDF salt, in hex, that the existing values use. Default: STRATUM_HKDF_SALT. |
new_salt |
No | The HKDF salt, in hex, that the rotation encrypts under. Default: STRATUM_HKDF_SALT. |
A salt must be a non-empty, even-length hex string. To change only the salt, give the same value as old_key and new_key, and two different salts. The route answers 400 VALIDATION_ERROR when a salt is not valid hex, or when the key and the salt both stay the same.
Response: 200 OK
{ "config_entries_rotated": 12, "webhooks_rotated": 5, "already_rotated": 0, "unreadable": [ { "table": "config_entries", "id": "7f1c2e9a-3b4d-4e5f-8a6b-9c0d1e2f3a4b" } ]}already_rotated: values that already decrypt with the new key, for example after an interrupted request.unreadable: values that decrypt with neither key. They stay unchanged. Examine each one before you remove the old key.
If encrypted values exist and none of them decrypts with either key, the request returns 400 VALIDATION_ERROR and changes no row. This result almost always means that old_key is wrong.
After rotation, update the STRATUM_ENCRYPTION_KEY environment variable and restart the control plane.