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:
| {
"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
| {
"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.
| {
"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
| {
"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.
| {
"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).
| {
"expires_at": "2026-05-23T00:00:00Z",
"days_remaining": 7,
"threshold": "7d"
}
|
secret.expired
| {
"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.
| {
"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
| {
"policy_id": "pol_abc",
"policy_name": "production-read-only",
"actor": "user:admin@acme.example"
}
|
policy.binding.created / policy.binding.deleted
| {
"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.
| {
"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.
| {
"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:
| {
"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.
| {
"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).
| {
"rotation_policy_id": "rot_quarterly",
"secrets_rotated": [
{"path": "...", "old_version": 3, "new_version": 4}
],
"started_at": "...",
"completed_at": "..."
}
|
rotation.failed
| {
"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
| {
"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
| {
"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
| {
"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
| {
"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
| {
"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
| {
"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
| {
"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
| {
"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).
| {
"lease_id": "lease_abc",
"engine": "support-db",
"role": "readonly",
"expired_at": "2026-04-23T22:00:00Z"
}
|
Federation
federation.sync.completed
| {
"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
| {
"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
| {
"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.
| {
"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:
| "events": ["secret.created", "secret.updated", "secret.deleted", "secret.rotated", "secret.expiring", "secret.expired"]
|
Or use a wildcard:
* 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
| 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.