Rotation Policies
Define schedules for automated rotation, attach secrets, subscribe to events, trigger immediate rotations. For the conceptual model, see Rotation.
Base path: /v1/rotation/
Create a rotation policy
1
2
3
4
5
6
7
8
9
10
11
12 | 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"]
}'
|
| 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,
},
)
|
1
2
3
4
5
6
7
8
9
10
11
12
13 | 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:
1
2
3
4
5
6
7
8
9
10
11
12 | {
"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.
| 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:
| 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:
| 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
| curl -H "Authorization: Bearer $TOKEN" \
"https://scaivault.scailabs.ai/v1/rotation/policies?active_only=true"
|
Enable / disable
Pause a policy without detaching secrets:
| curl -X POST https://scaivault.scailabs.ai/v1/rotation/policies/rot_quarterly/disable \
-H "Authorization: Bearer $TOKEN"
|
Resume:
| 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.
For the whole policy:
| curl -X POST https://scaivault.scailabs.ai/v1/rotation/policies/rot_quarterly/rotate \
-H "Authorization: Bearer $TOKEN"
|
Or scoped to specific paths:
| 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):
| 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
| curl -H "Authorization: Bearer $TOKEN" \
https://scaivault.scailabs.ai/v1/rotation/policies/rot_quarterly/history
|
Response:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | {
"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:
| 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:
| curl -H "Authorization: Bearer $TOKEN" \
"https://scaivault.scailabs.ai/v1/rotation/due?within_hours=168&include_overdue=true"
|
Response:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | {
"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
| 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
| 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
| 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
| 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | 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