Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Webhook Events

Every event ScaiVault can emit, with the exact JSON shape of the data field. Use this when building a webhook receiver — your handler should branch on event_type and parse data accordingly.

For envelope structure, signature verification, and delivery semantics, see Webhooks Reference and Webhook Signatures.

Envelope#

Common fields on every event:

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "event_id": "evt_01HK7X9Z...",
  "event_type": "secret.rotated",
  "timestamp": "2026-04-23T14:00:00.123456Z",
  "tenant_id": "tnt_acme_prod",
  "partner_id": "ptn_acme",
  "identity_id": "system:rotation-scheduler",
  "path": "environments/production/salesforce/oauth",
  "data": { /* event-type-specific */ },
  "request_id": "req_abc123"
}

path is the resource path where applicable (secrets, certs); for events about non-path resources (policies, engines), it's omitted.

Secrets#

secret.created#

json
1
2
3
4
5
6
7
{
  "version": 1,
  "secret_type": "json",
  "created_by": "user:alice@acme.example",
  "tags": ["salesforce", "production"],
  "expires_at": null
}

secret.updated#

A new version written to an existing path.

json
1
2
3
4
5
6
{
  "old_version": 3,
  "new_version": 4,
  "updated_by": "sa:provisioner",
  "is_rotation": false
}

is_rotation: true when the update came through a rotation policy. For human-driven updates, false.

secret.deleted#

json
1
2
3
4
5
6
{
  "version_count": 4,
  "deleted_by": "user:bob@acme.example",
  "permanent": false,
  "recoverable_until": "2026-05-23T15:00:00Z"
}

permanent: true means hard delete; no recoverable_until.

secret.rotated#

The new version came from a rotation policy. Distinct from secret.updated for filtering convenience.

json
1
2
3
4
5
6
7
8
{
  "old_version": 3,
  "new_version": 4,
  "rotation_policy_id": "rot_quarterly",
  "rotation_policy_name": "quarterly",
  "grace_period_ends": "2026-04-25T14:00:00Z",
  "auto_generated": true
}

secret.expiring#

Fires at configured thresholds before expires_at. The exact thresholds come from the secret's notify_before field (defaults: 30d, 7d, 1d).

json
1
2
3
4
5
{
  "expires_at": "2026-05-23T00:00:00Z",
  "days_remaining": 7,
  "threshold": "7d"
}

secret.expired#

json
1
2
3
4
5
{
  "expired_at": "2026-04-23T00:00:00Z",
  "version": 3,
  "auto_deleted": false
}

secret.accessed#

A read happened. Noisy by default. Use webhook filters (path_prefix) or subscriptions for one path, never enable globally.

json
1
2
3
4
5
6
{
  "version": 3,
  "accessed_by": "sa:reporting",
  "source_ip": "10.0.1.50",
  "user_agent": "scaivault-python/1.0"
}

Policies#

policy.created / policy.updated / policy.deleted#

json
1
2
3
4
5
{
  "policy_id": "pol_abc",
  "policy_name": "production-read-only",
  "actor": "user:admin@acme.example"
}

policy.binding.created / policy.binding.deleted#

json
1
2
3
4
5
6
7
{
  "policy_id": "pol_abc",
  "binding_id": "bind_xyz",
  "identity_type": "group",
  "identity_id": "group:developers",
  "actor": "user:admin@acme.example"
}

policy.violation#

An access was denied. Useful for alerting on anomalies — when a service that should work suddenly can't, this fires.

json
1
2
3
4
5
6
7
8
9
{
  "policy_id": null,
  "identity_id": "sa:reporting",
  "path": "infra/db/primary/credentials",
  "permission": "read",
  "reason": "no_matching_rule",
  "failed_condition": null,
  "source_ip": "10.0.1.50"
}

reason values: no_matching_rule, condition_failed, insufficient_scope. When condition_failed, failed_condition names which (ip_not_allowed, mfa_required, time_window_violation).

Rotation#

rotation.due#

Fires at each warn_before threshold and at actual rotation time. Distinguish via due_now.

json
1
2
3
4
5
6
7
8
{
  "rotation_policy_id": "rot_quarterly",
  "rotation_policy_name": "quarterly",
  "next_rotation_at": "2026-04-30T00:00:00Z",
  "threshold": "7d",
  "due_now": false,
  "auto_generate": false
}

When due_now: true:

json
1
2
3
4
5
6
7
{
  "rotation_policy_id": "rot_quarterly",
  "next_rotation_at": "2026-04-30T00:00:00Z",
  "threshold": "0",
  "due_now": true,
  "auto_generate": false
}

If auto_generate: false, this is your cue to write a new value via PUT. If auto_generate: true, ScaiVault is generating it; this event is informational.

rotation.overdue#

auto_generate: false rotation that exceeded its grace period without a new value being written.

json
1
2
3
4
5
6
{
  "rotation_policy_id": "rot_quarterly",
  "due_at": "2026-04-30T00:00:00Z",
  "grace_ended_at": "2026-05-02T00:00:00Z",
  "overdue_hours": 6
}

Alert on this. It means the rotation pipeline is broken.

rotation.completed#

A rotation succeeded. Paired with secret.rotated (rotation.completed has rotation-policy context, secret.rotated has secret context).

json
1
2
3
4
5
6
7
8
{
  "rotation_policy_id": "rot_quarterly",
  "secrets_rotated": [
    {"path": "...", "old_version": 3, "new_version": 4}
  ],
  "started_at": "...",
  "completed_at": "..."
}

rotation.failed#

json
1
2
3
4
5
6
7
{
  "rotation_policy_id": "rot_quarterly",
  "secret_path": "environments/production/salesforce/oauth",
  "error_code": "webhook_delivery_failed",
  "error_message": "Webhook returned 502 after 7 retries",
  "next_retry_at": null
}

next_retry_at: null means retries are exhausted; manual intervention needed.

Certificates#

certificate.issued#

json
1
2
3
4
5
6
7
8
9
{
  "certificate_id": "cert_abc",
  "common_name": "billing.svc.cluster.local",
  "issuer_ca_id": "ca_intermediate_mtls",
  "serial_number": "1A:2B:...",
  "not_before": "2026-04-23T14:00:00Z",
  "not_after": "2026-04-30T14:00:00Z",
  "issued_via": "internal_ca"
}

issued_via: internal_ca, acme, csr_sign, import.

certificate.renewed#

json
1
2
3
4
5
6
7
8
{
  "old_certificate_id": "cert_abc",
  "new_certificate_id": "cert_def",
  "common_name": "api.acme.example",
  "not_after": "2026-07-22T14:00:00Z",
  "renewal_method": "acme_auto",
  "days_before_expiry": 32
}

certificate.revoked#

json
1
2
3
4
5
6
7
8
{
  "certificate_id": "cert_abc",
  "common_name": "billing.svc.cluster.local",
  "serial_number": "1A:2B:...",
  "reason": "key_compromise",
  "revoked_by": "user:admin@acme.example",
  "crl_updated_at": "2026-04-23T14:30:00Z"
}

certificate.expiring#

json
1
2
3
4
5
6
7
8
9
{
  "certificate_id": "cert_abc",
  "common_name": "api.acme.example",
  "not_after": "2026-05-23T00:00:00Z",
  "days_remaining": 30,
  "threshold": "30d",
  "is_acme_managed": true,
  "auto_renew": true
}

If is_acme_managed: true and auto_renew: true, ScaiVault will handle this. Otherwise, you need to act.

certificate.renewal_failed#

json
1
2
3
4
5
6
7
8
{
  "certificate_id": "cert_abc",
  "common_name": "api.acme.example",
  "error_code": "acme_challenge_failed",
  "error_message": "DNS-01 propagation timeout after 300s",
  "attempt": 3,
  "next_retry_at": "2026-04-23T16:00:00Z"
}

Dynamic secrets#

dynamic.lease.created#

json
1
2
3
4
5
6
7
8
{
  "lease_id": "lease_abc",
  "engine": "support-db",
  "role": "readonly",
  "issued_to": "sa:reporting",
  "ttl_seconds": 7200,
  "expires_at": "2026-04-23T22:00:00Z"
}

dynamic.lease.renewed#

json
1
2
3
4
5
6
{
  "lease_id": "lease_abc",
  "previous_expires_at": "2026-04-23T22:00:00Z",
  "new_expires_at": "2026-04-23T23:00:00Z",
  "increment_seconds": 3600
}

dynamic.lease.revoked#

json
1
2
3
4
5
6
{
  "lease_id": "lease_abc",
  "revoked_by": "sa:reporting",
  "reason": "explicit",
  "revocation_succeeded": true
}

reason: explicit (DELETE), expired, revoke_prefix, engine_disabled. revocation_succeeded: false means the source-system revocation statement errored — ScaiVault has marked the lease revoked but the underlying user may still exist.

dynamic.lease.expired#

Fires when a lease passes its TTL naturally (no explicit revoke).

json
1
2
3
4
5
6
{
  "lease_id": "lease_abc",
  "engine": "support-db",
  "role": "readonly",
  "expired_at": "2026-04-23T22:00:00Z"
}

Federation#

federation.sync.completed#

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "backend_id": "fed_abc",
  "backend_name": "hashicorp-production",
  "started_at": "...",
  "completed_at": "...",
  "secrets_synced": 127,
  "secrets_created": 5,
  "secrets_updated": 12,
  "secrets_deleted": 0,
  "conflicts": 0
}

federation.sync.failed#

json
1
2
3
4
5
6
7
8
{
  "backend_id": "fed_abc",
  "started_at": "...",
  "failed_at": "...",
  "error_code": "backend_timeout",
  "error_message": "Vault upstream did not respond within 30s",
  "next_attempt_at": "2026-04-23T15:00:00Z"
}

federation.backend.unreachable#

json
1
2
3
4
5
6
{
  "backend_id": "fed_abc",
  "consecutive_failures": 3,
  "last_success_at": "2026-04-22T14:00:00Z",
  "next_attempt_at": "2026-04-23T14:30:00Z"
}

Fires once when persistent failure threshold (default: 3 consecutive) is hit. Won't refire until the backend recovers and fails again.

Identity (system events)#

identity.sync.completed#

ScaiKey-driven identity cache update finished.

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "sync_id": 42,
  "sync_type": "FULL",
  "tenant_id": "tnt_acme_prod",
  "partners_synced": 1,
  "tenants_synced": 1,
  "users_synced": 150,
  "groups_synced": 25,
  "duration_ms": 4500
}

Subscribing to subsets#

events is exact-match by default. To subscribe to all secret events:

json
1
"events": ["secret.created", "secret.updated", "secret.deleted", "secret.rotated", "secret.expiring", "secret.expired"]

Or use a wildcard:

json
1
"events": ["secret.*"]

* matches one segment after the dot. ** matches multiple segments. The full event-type vocabulary uses one level of nesting, so * covers everything in practice.

Listing event types programmatically#

bash
1
2
curl -H "Authorization: Bearer $TOKEN" \
     https://scaivault.scailabs.ai/v1/events/types

Returns the canonical list. Useful for building event-picker UIs that stay current.

Updated 2026-05-17 13:26:51 View source (.md) rev 1