---
title: Rotation Policies
path: api-guides/rotation
status: published
---

# Rotation Policies

Define schedules for automated rotation, attach secrets, subscribe to events, trigger immediate rotations. For the conceptual model, see [Rotation](../core-concepts/rotation).

**Base path:** `/v1/rotation/`

## Create a rotation policy

```bash
curl -X POST https://scaivault.scailabs.ai/v1/rotation/policies \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "quarterly",
    "description": "90-day rotation for production credentials",
    "interval": "90d",
    "grace_period": "48h",
    "warn_before": "7d,1d",
    "auto_generate": false,
    "webhook_ids": ["wh_rotation_alerts"]
  }'
```

```python
resp = httpx.post(
    "https://scaivault.scailabs.ai/v1/rotation/policies",
    headers={"Authorization": f"Bearer {os.environ['SCAIVAULT_TOKEN']}"},
    json={
        "name": "quarterly",
        "interval": "90d",
        "grace_period": "48h",
        "warn_before": "7d,1d",
        "auto_generate": False,
    },
)
```

```typescript
const resp = await fetch("https://scaivault.scailabs.ai/v1/rotation/policies", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SCAIVAULT_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "quarterly",
    interval: "90d",
    grace_period: "48h",
    auto_generate: false,
  }),
});
```

Response:

```json
{
  "id": "rot_quarterly",
  "name": "quarterly",
  "interval": "90d",
  "interval_seconds": 7776000,
  "grace_period": "48h",
  "warn_before": "7d,1d",
  "auto_generate": false,
  "is_active": true,
  "secrets_count": 0,
  "created_at": "2026-04-23T14:00:00Z"
}
```

### Fields

| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Unique within tenant |
| `interval` | Yes | How often (`90d`, `7d`, `24h`) |
| `description` | No | |
| `grace_period` | No | Default `48h`. Previous version remains readable during this window |
| `warn_before` | No | Comma-separated durations before due date (`7d,1d`) |
| `auto_generate` | No | If true, ScaiVault writes a random new value. If false, event-driven — your automation writes the new version |
| `secret_policy_id` | No | Value-generation policy (for `auto_generate: true`) |
| `webhook_ids` | No | Webhooks that receive rotation events |

## Assign secrets to a policy

Rotation policies don't do anything until secrets are attached.

```bash
curl -X POST https://scaivault.scailabs.ai/v1/rotation/policies/rot_quarterly/secrets \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"secret_path": "environments/production/salesforce/oauth"}'
```

Also permitted: assign at write time by setting `options.rotation_policy_id`:

```bash
curl -X PUT https://scaivault.scailabs.ai/v1/secrets/environments/production/salesforce/oauth \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {"client_id": "...", "client_secret": "..."},
    "options": {"rotation_policy_id": "rot_quarterly"}
  }'
```

Detach:

```bash
curl -X DELETE https://scaivault.scailabs.ai/v1/rotation/policies/rot_quarterly/secrets/environments%2Fproduction%2Fsalesforce%2Foauth \
  -H "Authorization: Bearer $TOKEN"
```

(Path is URL-encoded — the `/` becomes `%2F`.)

## List policies

```bash
curl -H "Authorization: Bearer $TOKEN" \
     "https://scaivault.scailabs.ai/v1/rotation/policies?active_only=true"
```

## Enable / disable

Pause a policy without detaching secrets:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/rotation/policies/rot_quarterly/disable \
  -H "Authorization: Bearer $TOKEN"
```

Resume:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/rotation/policies/rot_quarterly/enable \
  -H "Authorization: Bearer $TOKEN"
```

Disabled policies don't rotate. Scheduled rotations that fire while disabled are skipped; they don't queue up.

## Trigger immediate rotation

For the whole policy:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/rotation/policies/rot_quarterly/rotate \
  -H "Authorization: Bearer $TOKEN"
```

Or scoped to specific paths:

```bash
curl -X POST https://scaivault.scailabs.ai/v1/rotation/policies/rot_quarterly/rotate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"secret_paths": ["environments/production/salesforce/oauth"]}'
```

Or per-secret (doesn't require a policy attached):

```bash
curl -X POST https://scaivault.scailabs.ai/v1/secrets/app/db/password/rotate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason": "compromise", "new_value": {"password": "..."}, "grace_period": "1h"}'
```

## List rotation history

```bash
curl -H "Authorization: Bearer $TOKEN" \
     https://scaivault.scailabs.ai/v1/rotation/policies/rot_quarterly/history
```

Response:

```json
{
  "data": [
    {
      "id": "rh_abc",
      "policy_id": "rot_quarterly",
      "secret_path": "environments/production/salesforce/oauth",
      "status": "success",
      "old_version": 3,
      "new_version": 4,
      "rotated_at": "2026-04-23T00:00:00Z",
      "rotated_by": "system:rotation-scheduler"
    }
  ],
  "has_more": false
}
```

Filter by status:

```bash
curl -H "Authorization: Bearer $TOKEN" \
     "https://scaivault.scailabs.ai/v1/rotation/policies/rot_quarterly/history?status=failed"
```

## Find secrets due for rotation

Use this in dashboards or scheduled jobs:

```bash
curl -H "Authorization: Bearer $TOKEN" \
     "https://scaivault.scailabs.ai/v1/rotation/due?within_hours=168&include_overdue=true"
```

Response:

```json
{
  "secrets": [
    {
      "path": "environments/production/salesforce/oauth",
      "policy_id": "rot_quarterly",
      "next_rotation_at": "2026-04-30T00:00:00Z",
      "is_overdue": false
    },
    {
      "path": "environments/staging/db-password",
      "policy_id": "rot_monthly",
      "next_rotation_at": "2026-04-20T00:00:00Z",
      "is_overdue": true
    }
  ]
}
```

## End-to-end: event-driven rotation for an OAuth credential

### 1. Create the rotation policy

```bash
curl -X POST https://scaivault.scailabs.ai/v1/rotation/policies \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "oauth-90d",
    "interval": "90d",
    "grace_period": "48h",
    "warn_before": "7d,1d",
    "auto_generate": false
  }'
```

### 2. Register a webhook

```bash
curl -X POST https://scaivault.scailabs.ai/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "oauth-rotation-handler",
    "url": "https://ops.acme.example/scaivault/rotate-oauth",
    "secret": "whsec_...",
    "events": ["rotation.due"]
  }'
```

### 3. Attach the webhook to the policy

```bash
curl -X PATCH https://scaivault.scailabs.ai/v1/rotation/policies/rot_oauth-90d \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"webhook_ids": ["wh_oauth-rotation-handler"]}'
```

### 4. Attach the secret

```bash
curl -X POST https://scaivault.scailabs.ai/v1/rotation/policies/rot_oauth-90d/secrets \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"secret_path": "integrations/salesforce/oauth"}'
```

### 5. Handle the webhook

```python
from fastapi import FastAPI, Request
import httpx

app = FastAPI()

@app.post("/scaivault/rotate-oauth")
async def on_rotation(request: Request):
    event = await request.json()
    if event["event_type"] != "rotation.due":
        return {"ok": True}
    if not event["data"].get("due_now"):
        # This is a "7d before" or "1d before" warning — just log it
        return {"ok": True}

    path = event["path"]  # e.g. "integrations/salesforce/oauth"
    # 1. Obtain new OAuth credential from Salesforce admin portal, or via SF API
    new_creds = get_new_salesforce_credentials()
    # 2. Write it back
    httpx.put(
        f"https://scaivault.scailabs.ai/v1/secrets/{path}",
        headers={"Authorization": f"Bearer {TOKEN}"},
        json={"data": new_creds, "secret_type": "json"},
    )
    return {"ok": True}
```

That's the full loop — ScaiVault schedules, your automation generates, ScaiVault records.

## Common error codes

| Code | When |
|------|------|
| `rotation_policy_not_found` | |
| `secret_not_found` | Assigning a non-existent secret |
| `name_conflict` | Policy name already exists |
| `invalid_duration` | Interval or grace_period didn't parse |

## What's next

- [Rotation](../core-concepts/rotation) — concepts.
- [Events and Webhooks](../core-concepts/events-and-webhooks) — consuming rotation events.
- [Rotation Reference](../reference/rotation) — all endpoints.
