OAuth secret rotation
Issue a new client_secret for your Sign-in-with-Quota app while the previous one keeps working for 30 days. Roll the new value through CI on your own schedule, then either let the old one expire or revoke it immediately if you suspect compromise.
01 How the grace period works
Each oauth_clients row stores up to two secret hashes:
client_secret_hash— the primary (current) secret.client_secret_hash_secondary— the previous secret, kept alive untilsecondary_expires_at.
At POST /oauth/token the server compares your submitted secret against the primary first; on mismatch, it falls back to the secondary if and only if secondary_expires_at is strictly in the future. Both compares are timing-safe, so an attacker cannot tell from response timing which slot they hit.
Calling POST /developers/apps/:id/rotate-secret performs an atomic swap:
- Generate a fresh plaintext secret server-side.
- Copy the existing primary hash into the secondary slot, set
secondary_expires_at = NOW() + INTERVAL '30 days'. - Store the new hash as primary.
- Return the new plaintext to the caller, once.
02 Operational flow
The intended sequence for a non-breaking rotation:
- Call
POST /developers/apps/:id/rotate-secretand capture the new plaintext. - Update your secret store (Vault, AWS Secrets Manager,
.envin CI, etc.) with the new value. - Redeploy every service that uses the old secret. You have 30 days.
- Optionally call
POST /developers/apps/:id/revoke-secondary-secretonce you have confirmed every deployment is on the new secret. If you skip this step, the old secret stops being accepted automatically atsecondary_expires_at.
03 Authentication
Both endpoints require a developer session cookie (set by POST /auth/login). Bearer API keys are not accepted — rotation is an account-owner operation, not an app operation. The server checks that oauth_clients.developer_id matches your session user; otherwise responds 403 forbidden.
Each endpoint is rate-limited per session: rotate is 5 per minute, revoke is 10 per minute.
04 Rotate the secret
Path parameters
| idstringrequired | The app’s internal UUID (the id field returned by GET /developers/apps). Not the public-facing client_id. |
Request
curl -X POST https://api.usequota.ai/developers/apps/$APP_ID/rotate-secret \
-H "Cookie: quota_session=$SESSION_TOKEN"Response — 200 OK
{
"client_secret": "quota_secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"secondary_expires_at": "2026-06-08T17:42:13.000Z"
}| client_secretstring | The new plaintext secret. Returned only here, never readable again. |
| secondary_expires_atstring (ISO-8601) | When the previous secret stops being accepted. Always approximately 30 days in the future. Compute with NOW() + INTERVAL '30 days' server-side, so the exact timestamp is the server’s wall clock at request time. |
05 Revoke the previous secret
Use this endpoint when you want to terminate the grace period early — typically because you suspect the previous secret was leaked or compromised. After revocation, only the current primary secret is accepted at POST /oauth/token.
Request
curl -X POST https://api.usequota.ai/developers/apps/$APP_ID/revoke-secondary-secret \
-H "Cookie: quota_session=$SESSION_TOKEN"Response — 204 No Content
Empty body. The secondary hash and its expiry timestamp are cleared atomically. Calling revoke when no secondary is set is a no-op (still returns 204).
06 Errors
POST /auth/login.