---
title: Events and Webhooks
path: core-concepts/events-and-webhooks
status: published
---

# Events and Webhooks

ScaiVault emits events for everything important that happens — secrets written, rotated, accessed; policies changed; certificates issued or expiring; dynamic leases granted or revoked. You consume them via webhooks (HTTP POST to your endpoint) or subscriptions (long-polling channel scoped to specific paths).

## Delivery mechanisms

| Mechanism | Use when |
|-----------|----------|
| **Webhooks** | You have a reachable HTTPS endpoint. Deliveries retried with backoff, signed with HMAC. |
| **Subscriptions** | You want events filtered to specific secret paths, or you need long-polling (behind firewalls that can't receive inbound HTTP). |

Both deliver the same event payloads. Pick whichever fits your deployment.

## Event catalog

**Secret events**

- `secret.created` — new path written for the first time.
- `secret.updated` — new version written on an existing path.
- `secret.deleted` — soft or hard delete.
- `secret.rotated` — rotation completed (a new version was written by a rotation policy).
- `secret.expiring` — a secret's `expires_at` is approaching (emitted at configurable thresholds).
- `secret.expired` — a secret's `expires_at` has passed.
- `secret.accessed` — a read happened. Noisy; use subscriptions with a filter or consume from the audit log instead.

**Policy events**

- `policy.created`, `policy.updated`, `policy.deleted`
- `policy.binding.created`, `policy.binding.deleted`
- `policy.violation` — an access was denied by a policy evaluation. Useful for alerting on anomalies.

**Rotation events**

- `rotation.due` — a rotation is approaching or due. Fires at each configured `warn_before` threshold, then with `due_now: true` when the rotation fires.
- `rotation.overdue` — a rotation that required manual intervention (`auto_generate: false`) missed its window.
- `rotation.completed` — rotation successful.
- `rotation.failed` — rotation tried and errored; included error details.

**Certificate events**

- `certificate.issued` — new cert minted (internal CA or ACME).
- `certificate.renewed` — ACME auto-renew succeeded.
- `certificate.revoked`
- `certificate.expiring` — expires in the configured warning window.

**Dynamic events**

- `dynamic.lease.created`
- `dynamic.lease.renewed`
- `dynamic.lease.revoked`
- `dynamic.lease.expired`

**Federation events**

- `federation.sync.completed`
- `federation.sync.failed`
- `federation.backend.unreachable`

## Event envelope

```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": {
    "old_version": 3,
    "new_version": 4,
    "rotation_policy_id": "rot_quarterly",
    "grace_period_ends": "2026-04-25T14:00:00Z"
  },
  "request_id": "req_abc123"
}
```

Fields:

- `event_id` — unique, monotonic-ish. Use for idempotency.
- `event_type` — the catalog name above.
- `path` — relevant resource path (secret, cert, etc.).
- `data` — event-type-specific payload. See reference for per-event schemas.
- `identity_id` — who triggered the event (user, service account, or `system:*`).

## Webhooks

Register a webhook once:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "rotation-alerts",
    "url": "https://ops.acme.example/scaivault/webhook",
    "secret": "whsec_ReplaceThisWithARandom32ByteString",
    "events": ["secret.rotated", "secret.expiring", "rotation.overdue"],
    "filters": {
      "path_prefix": "environments/production/"
    }
  }'
```

Every matching event is POSTed to the URL. The request includes headers:

```
POST /scaivault/webhook HTTP/1.1
Host: ops.acme.example
Content-Type: application/json
X-ScaiVault-Event-Id: evt_01HK7X9Z...
X-ScaiVault-Event-Type: secret.rotated
X-ScaiVault-Signature: sha256=...
X-ScaiVault-Timestamp: 1714478400
User-Agent: ScaiVault-Webhook/1.0
```

Your endpoint must return a 2xx within 10 seconds to count as delivered. Non-2xx or timeout triggers retry with exponential backoff: 30s, 5min, 30min, 2h, 8h, 24h, then marked as failed.

```mermaid
sequenceDiagram
    participant SV as ScaiVault
    participant Hook as Your endpoint

    SV->>Hook: POST event (attempt 1)
    Hook-->>SV: 502 / timeout
    Note over SV: wait 30s
    SV->>Hook: POST event (attempt 2)
    Hook-->>SV: 502
    Note over SV: wait 5min
    SV->>Hook: POST event (attempt 3)
    Hook-->>SV: 200 OK
    Note over SV: delivery succeeded;<br/>recorded in deliveries
```

See [Webhook Signatures](../advanced/webhook-signatures) for how to verify the HMAC signature — this is **required** to trust the payload.

### Managing webhooks

- `GET /v1/webhooks` — list
- `GET /v1/webhooks/{id}` — details including delivery statistics
- `PATCH /v1/webhooks/{id}` — update URL, events, filters
- `DELETE /v1/webhooks/{id}` — delete
- `POST /v1/webhooks/{id}/test` — send a test event
- `GET /v1/webhooks/{id}/deliveries` — per-delivery history

## Subscriptions

Subscriptions scope to specific paths (or path prefixes) and deliver via webhook *or* long-polling. Use them when you care about a subset of paths and don't want global webhook noise.

```bash
curl -X POST https://scaivault.scailabs.ai/v1/subscriptions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "reporting-sf-rotations",
    "paths": ["integrations/salesforce/*"],
    "events": ["secret.rotated", "secret.updated"],
    "delivery": {
      "type": "polling"
    }
  }'
```

Then poll:

```bash
curl -H "Authorization: Bearer $TOKEN" \
     "https://scaivault.scailabs.ai/v1/subscriptions/sub_abc/poll?timeout=30"
```

The request holds until an event arrives or the timeout elapses, whichever is first. On response, the events are returned. Your next poll uses `since_event_id` to continue.

Polling is useful behind firewalls that can't accept inbound HTTP, or in cases where pull semantics are simpler than managing a webhook receiver.

## Event filtering

Both webhooks and subscriptions accept filters:

```json
"filters": {
  "path_prefix": "environments/production/",
  "secret_type": "certificate",
  "tags": ["critical"]
}
```

Available filter keys depend on the event type. See [Webhooks Reference](../reference/webhooks) for the full list.

## Idempotency

Webhook delivery is **at least once** — you may receive the same event more than once if your endpoint times out after receiving. Use `event_id` to deduplicate.

```python
seen_events = set()

def handle_event(event):
    if event["event_id"] in seen_events:
        return  # duplicate
    seen_events.add(event["event_id"])
    process(event)
```

In production, persist `event_id` in a store with a reasonable TTL (e.g. Redis with 24h expiry).

## Ordering

Events are delivered roughly in timestamp order, but under load order is not guaranteed. Consumers that care about ordering (e.g., `secret.updated` followed by `secret.rotated`) should either:

- Order by `timestamp` in a buffer before processing.
- Re-fetch current state from the API rather than relying purely on event diffs.

## What's next

- [Webhook Signatures](../advanced/webhook-signatures) — HMAC verification.
- [Webhooks Reference](../reference/webhooks) — endpoint details.
- [Subscriptions Reference](../reference/subscriptions) — long-polling API.
