Skip to content

Webhooks API

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
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
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

POST /api/v1/webhooks

Body:

{
"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.


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.


PATCH /api/v1/webhooks/:id

Body (all optional):

{
"url": "https://new-endpoint.com/hook",
"events": ["tenant.created", "tenant.updated"],
"secret": "new-signing-secret"
}

Response: 200 OK. Returns the updated webhook.


POST /api/v1/webhooks/:id/test

Sends a synthetic test event to the webhook URL and reports the response.

Response: 200 OK

{
"success": true,
"response_code": 200
}

GET /api/v1/webhooks/deliveries/stats

Response: 200 OK

{
"total": 150,
"pending": 5,
"success": 130,
"failed": 15
}

GET /api/v1/webhooks/deliveries/failed?limit=100

Returns 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.


POST /api/v1/webhooks/deliveries/retry-all

Resets all failed deliveries for retry (clears attempt counter, sets status to pending).

Response: 200 OK

{
"retried": 15
}

POST /api/v1/webhooks/deliveries/:deliveryId/retry

Response: 200 OK with {"success": true}


GET /api/v1/audit-logs

Query 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].


POST /api/v1/maintenance/rotate-encryption-key

Re-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.