Rotation SOP — Stripe Restricted API Key
Mode: operator-assisted Last validated: 2026-04-24 UTC Validation method: read-only-docs (NEVER live-rotate without grace period + sandbox first) Average duration: 10m Required role: superadmin
Applies to: STRIPE_RESTRICTED_KEY (and any per-environment restricted keys, e.g., STRIPE_RESTRICTED_KEY_LIVE, STRIPE_RESTRICTED_KEY_TEST). The Stripe master / secret key is never stored in our infrastructure and is not in scope here.
Stripe rotation has a built-in grace period. Use it. Selecting "Now" on rotation deletes the old key instantly and risks a billing outage if propagation fails. Always specify a grace period (recommended: 24h) so old + new keys coexist during the swap window.
When to run
- Scheduled rotation (cadence: every 90 days)
- Operator-initiated (suspected compromise, off-cycle — use grace period 1h instead of 24h for compromise scenarios so old key dies faster but propagation still has a window)
- After incident (employee offboarding)
Prerequisites
- [ ] Stripe dashboard access via SSO with email/SMS verification on hand
- [ ] Restricted key has appropriate scopes for our use case (per-resource Read/Write)
- [ ] Existing key in Infisical with history
- [ ] Downstream consumer list (Raptor billing routes, webhooks, subscription engine)
- [ ] Confirmation that the key being rotated is the restricted key, not the secret/master key (Stripe master keys are not stored anywhere — they live only in the dashboard for human use)
Steps
1. Pre-rotation checks
# Confirm current restricted key works
curl -sS -u "$CURRENT_RESTRICTED_KEY:" https://api.stripe.com/v1/customers?limit=1 | jq '.data | length'
# Expect: 0 or 1 (or whatever the test query returns; HTTP 200 is what matters)
Confirm the key's scopes match what we expect (visible in the dashboard's key detail panel). Document the scope set in the audit context.
2. Generate the new credential (rotate with grace period)
- Navigate to
https://dashboard.stripe.com/apikeys(live) orhttps://dashboard.stripe.com/test/apikeys(test). - Locate the restricted key by name.
- Click the overflow menu (
⋯) → "Rotate key". - Select expiration date for the OLD key: choose 24 hours (or 1 hour for a compromise scenario). Avoid "Now" unless you are mitigating active exploitation.
- Click "Rotate API key".
- Copy the new key value immediately — shown once.
- Add a note in the dashboard documenting where the key is stored (e.g., "Infisical: prod/STRIPE_RESTRICTED_KEY").
The old key continues to work for the grace period; the new key works immediately.
3. Validate the new credential
NEW_RESTRICTED_KEY="..."
curl -sS -u "$NEW_RESTRICTED_KEY:" https://api.stripe.com/v1/customers?limit=1 | jq '.data | length'
# Expect: HTTP 200 with same shape as step 1.
Confirm scope (try a permitted call and a denied call):
# Permitted:
curl -sS -u "$NEW_RESTRICTED_KEY:" https://api.stripe.com/v1/subscriptions?limit=1 | jq '.object'
# Expect: "list"
# Denied (if scope excludes balance):
curl -sS -u "$NEW_RESTRICTED_KEY:" https://api.stripe.com/v1/balance | jq '.error.code'
# Expect: "permission_error" or whatever the denied scope returns.
4. Store in Infisical
infisical secrets set STRIPE_RESTRICTED_KEY="$NEW_RESTRICTED_KEY" \
--projectId="$INFISICAL_PROJECT_ID" --env=prod
5. Propagate to downstream consumers
| Consumer | How |
|---|---|
| Raptor (raxx-api-prod) | heroku config:set STRIPE_RESTRICTED_KEY="$NEW_RESTRICTED_KEY" -a raxx-api-prod |
| Staging (raxx-api-staging) | If using a separate test-mode key, rotate independently |
| Webhook signing secret | NOT this SOP — webhook secrets are rotated via a separate path (stripe-webhook-secret.md — to be created if/when we add webhooks) |
6. Verify downstream
# Hit a billing endpoint that uses Stripe
curl -sS https://api.raxx.app/api/billing/subscription | jq '.status'
# Expect: a successful response (not 401 / 500).
# Tail logs for any 401 / authentication_required from Stripe SDK
heroku logs --tail -a raxx-api-prod | grep -i stripe
# Expect: no auth errors after the dyno restart.
Run for at least 5 minutes to catch background jobs that may use the key.
7. Revoke the old credential
If the grace period was set to >0, the old key auto-expires at the deadline — no action needed. To explicitly expire earlier:
- Dashboard → API keys → old key → overflow → "Expire key".
- Confirm.
Verify:
curl -sS -o /dev/null -w "%{http_code}\n" -u "$OLD_RESTRICTED_KEY:" https://api.stripe.com/v1/customers?limit=1
# Expect: 401
8. Audit log entry
action: secret.rotate.completed
actor: <admin_id>
context: {
"secret_name": "STRIPE_RESTRICTED_KEY",
"method": "operator-assisted-dashboard",
"grace_period_hours": 24,
"old_key_expires_at": "..."
}
Rollback
If validation fails before the grace period ends, revert to the old key:
- Pull the previous value from Infisical version history.
heroku config:set STRIPE_RESTRICTED_KEY="$OLD_KEY" -a raxx-api-prod.- Wait for dyno restart.
- The old key continues to work until the grace period expires.
- Investigate the new key's failure; redo from step 2 with a fresh rotation.
After grace period expiry, the old key is dead. If the new key is broken at that point, generate a fresh restricted key (not a rotation — a brand new key with the same scopes) via Dashboard → "Create restricted key", and redo step 4+5.
Vendor doc references
- API keys: https://docs.stripe.com/keys
- Restricted keys: https://docs.stripe.com/keys#limit-access (or "Create restricted API keys")
- Dashboard: https://dashboard.stripe.com/apikeys
Known gotchas
- "Now" expiration kills the old key instantly. Use grace period for routine rotations.
- Restricted key scopes are immutable. To change scopes, create a new key with the new scopes — rotation preserves scopes.
- Webhook secrets are separate. This SOP does not cover endpoint signing secrets.
- Test mode and live mode have separate key sets. Confirm which mode the dashboard is in before rotating.
- Email/SMS verification is required at rotation time. Operator must have access to the verification destination.
- Notes in dashboard are persistent. Always update the storage-location note after rotation so a future operator knows where to look.
- The master/secret key is never in our codebase — it stays in Stripe's dashboard for human-only operations like adding new restricted keys. If you find it stored anywhere in our infra, that is a SEV-1 leak.