---
title: Webhook Events
path: reference/webhook-events
status: published
---

# Webhook Events Catalog

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](./webhooks) and [Webhook Signatures](../advanced/webhook-signatures).

## Envelope

Common fields on every event:

```json
{
  "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
{
  "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
{
  "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
{
  "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
{
  "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
{
  "expires_at": "2026-05-23T00:00:00Z",
  "days_remaining": 7,
  "threshold": "7d"
}
```

### `secret.expired`

```json
{
  "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
{
  "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
{
  "policy_id": "pol_abc",
  "policy_name": "production-read-only",
  "actor": "user:admin@acme.example"
}
```

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

```json
{
  "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
{
  "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
{
  "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
{
  "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
{
  "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
{
  "rotation_policy_id": "rot_quarterly",
  "secrets_rotated": [
    {"path": "...", "old_version": 3, "new_version": 4}
  ],
  "started_at": "...",
  "completed_at": "..."
}
```

### `rotation.failed`

```json
{
  "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
{
  "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
{
  "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
{
  "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
{
  "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
{
  "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
{
  "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
{
  "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
{
  "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
{
  "lease_id": "lease_abc",
  "engine": "support-db",
  "role": "readonly",
  "expired_at": "2026-04-23T22:00:00Z"
}
```

## Federation

### `federation.sync.completed`

```json
{
  "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
{
  "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
{
  "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
{
  "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
"events": ["secret.created", "secret.updated", "secret.deleted", "secret.rotated", "secret.expiring", "secret.expired"]
```

Or use a wildcard:

```json
"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
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.

## Related

- [Events and Webhooks (Concepts)](../core-concepts/events-and-webhooks)
- [Webhooks Reference](./webhooks)
- [Webhook Signatures](../advanced/webhook-signatures)
- [Subscriptions Reference](./subscriptions)
