API key rotation
Rotate an application’s client_secret from the dashboard or via API. The previous secret stays valid during a configurable grace period (default 15 minutes, max 60 minutes) so you can deploy the new secret without auth downtime.
When to rotate
- Compromised secret — rotate immediately, then set
grace_period_seconds: 0once you’ve confirmed no legitimate clients are still using the old one. - Quarterly hygiene — schedule a rotation every 90 days as part of your security review.
- After contractor offboarding — anyone who ever read the secret should no longer be able to use it.
Flow
sequenceDiagram
participant U as User (dashboard)
participant API as TryMellon API
participant CI as Your CI/CD
participant App as Your app
U->>API: POST /v1/applications/:id/rotate-secret
API-->>U: { new_client_secret, previous_secret_expires_at }
U->>CI: Update secret in vault
CI->>App: Deploy with new secret
Note over App,API: Both secrets accepted during grace window
API-->>App: Previous secret rejected after expires_at
Rotate from the dashboard
- Open Dashboard → Applications → [your app] → Settings.
- Section API keys → Rotate client secret.
- Confirm the warning modal.
- Copy the new secret immediately — it is shown once. The dashboard never displays it again.
- The previous secret expires at the timestamp shown (“previous expires in 14:53”). Deploy the new secret to all callers before the countdown hits zero.
Rotate from cURL
curl -X POST https://api.trymellonauth.com/v1/applications/$APP_ID/rotate-secret \
-H "Authorization: Bearer $TENANT_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rotation-$(date +%s)" \
-d '{ "grace_period_seconds": 900 }'
Response (200):
{
"ok": true,
"data": {
"application_id": "uuid",
"new_client_secret": "tm_secret_…",
"previous_secret_expires_at": "2026-04-15T15:14:00.000Z",
"rotated_at": "2026-04-15T15:00:00.000Z"
}
}
The Idempotency-Key header makes the call replay-safe — re-issuing the same request returns the cached response instead of generating a second secret.
CI/CD: zero-downtime rotation (GitHub Actions example)
name: Rotate TryMellon secret
on:
workflow_dispatch:
schedule:
- cron: '0 9 1 */3 *' # Quarterly, 1st day of month, 09:00 UTC
jobs:
rotate:
runs-on: ubuntu-latest
steps:
- name: Rotate secret
id: rotate
run: |
response=$(curl -sS -X POST \
"https://api.trymellonauth.com/v1/applications/${{ secrets.TM_APP_ID }}/rotate-secret" \
-H "Authorization: Bearer ${{ secrets.TM_TENANT_TOKEN }}" \
-H "Idempotency-Key: rotate-${{ github.run_id }}" \
-d '{"grace_period_seconds": 1800}')
echo "secret=$(echo "$response" | jq -r '.data.new_client_secret')" >> $GITHUB_OUTPUT
- name: Push to vault (example: GitHub Secrets)
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
run: |
gh secret set TM_CLIENT_SECRET --body "${{ steps.rotate.outputs.secret }}"
- name: Trigger production deploy
run: gh workflow run deploy.yml
The 30-minute grace window gives the deploy plenty of time to roll out before the previous secret stops working.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
403 SECRET_ROTATION_FORBIDDEN | Caller is not creator, tenant owner, or account owner. | Use a token from a user with one of those roles. |
| 401 after deploy | Old pods still picking up old secret from cache. | Confirm rolling restart completed; old secret remains valid until previous_secret_expires_at. |
| Two rotations in a row | Forgot to copy the secret first time. | Wait for the previous-secret window to expire, then rotate again. There’s no “show secret again” — by design. |
Audit trail
Every rotation emits the application.secret_rotated webhook event and is logged in the audit log (event = application.secret_rotated, metadata.rotated_by = userId). View it under Dashboard → Audit log.
Related
- Applications management — origins, webhook URL, lifecycle.
- Webhook events — full event catalog.
- Error codes —
SECRET_ROTATION_FORBIDDEN.