Regular key rotation limits the blast radius of a leaked credential. The recommended rotation interval is 90 days; rotate immediately if you suspect a key has been exposed.
Programmatic rotation uses the Management API, authenticated with a management key (mk-…) — create one in Dashboard → Settings → Management Keys. The management key manages your inference keys (sk-…); never use an inference key to call the management endpoints. You can also rotate manually in Dashboard → Keys.
Rotation is a three-step process: create the new key, propagate it to your services, then revoke the old key. Never revoke the old key before the new one is in place. There is no in-place "rotate" or update operation — rotation is always create new, cut over, delete old.
1. POST /v1/management/keys → new key (raw_key: sk-...)
2. Deploy new key to all services
3. Verify traffic is flowing on the new key
4. DELETE /v1/management/keys/{old_id}
curl -X POST https://opendunes.com/api/v1/management/keys \
-H "Authorization: Bearer $OPENDUNES_MANAGEMENT_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "production-v2",
"rate_limit_rpm": 60
}'
Response (201 Created):
{
"api_key": {
"id": "key_01j...",
"name": "production-v2",
"rate_limit_rpm": 60,
"created_at": "2026-06-23T08:00:00Z"
},
"raw_key": "sk-0a1b2c3d4e5f..."
}
Note. The raw_key value is shown once only at creation. Store it immediately in your secret manager (Vault, AWS Secrets Manager, etc.) — it cannot be retrieved again.
curl https://opendunes.com/api/v1/management/keys \
-H "Authorization: Bearer $OPENDUNES_MANAGEMENT_KEY"
{
"data": [
{ "id": "key_01h...", "name": "production-v1", "created_at": "2026-03-01T00:00:00Z", "last_used_at": "2026-06-23T07:59:00Z" },
{ "id": "key_01j...", "name": "production-v2", "created_at": "2026-06-23T08:00:00Z", "last_used_at": null }
]
}
The last_used_at field is how you confirm the new key is receiving traffic before revoking the old one.
import os, time, requests
BASE = "https://opendunes.com/api/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['OPENDUNES_MANAGEMENT_KEY']}",
"Content-Type": "application/json",
}
def create_key(name: str) -> dict:
r = requests.post(f"{BASE}/management/keys", json={"name": name}, headers=HEADERS)
r.raise_for_status()
return r.json()
def revoke_key(key_id: str) -> None:
r = requests.delete(f"{BASE}/management/keys/{key_id}", headers=HEADERS)
r.raise_for_status()
def wait_for_first_use(key_id: str, timeout: int = 300) -> bool:
deadline = time.time() + timeout
while time.time() < deadline:
r = requests.get(f"{BASE}/management/keys", headers=HEADERS)
r.raise_for_status()
for key in r.json()["data"]:
if key["id"] == key_id and key.get("last_used_at"):
return True
time.sleep(10)
return False
new = create_key("production-v2")
print(f"New key: {new['raw_key']}")
print("Deploy the new key and press Enter...")
input()
if wait_for_first_use(new["api_key"]["id"]):
print("New key is receiving traffic. Revoking old key.")
revoke_key(os.environ["OLD_KEY_ID"])
print("Done.")
else:
print("Timed out waiting for new key traffic. Check your deployment.")
To rotate with zero downtime:
- Run both keys simultaneously for a brief overlap period (5–15 minutes).
- During overlap, old-key traffic coexists with new-key traffic harmlessly — both keys draw from the same balance.
- Only revoke the old key after
last_used_at on the new key confirms it is active.
Revoking is permanent. If you are not sure everything has cut over, pause the old key in
Dashboard → Keys instead: it stops working immediately, exactly as a revoke would, but you can
switch it back on if something you had forgotten starts failing. Revoke it once you are confident.
Use descriptive, versioned names so you can identify keys in the export and analytics:
production-v1, production-v2
service-auth-v1, service-batch-v1
staging-2026-q3
Avoid default, test, or key1 — they become ambiguous in the activity export.
If you suspect a key is compromised:
curl -X DELETE "https://opendunes.com/api/v1/management/keys/$COMPROMISED_KEY_ID" \
-H "Authorization: Bearer $OPENDUNES_MANAGEMENT_KEY"
Revocation is immediate. Any request using the revoked key after this call receives HTTP 401 invalid_api_key. Replace it with a new key as quickly as possible to restore service.