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.
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.