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.
# 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.
https://dashboard.stripe.com/apikeys (live) or https://dashboard.stripe.com/test/apikeys (test).⋯) → "Rotate key".The old key continues to work for the grace period; the new key works immediately.
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.
infisical secrets set STRIPE_RESTRICTED_KEY="$NEW_RESTRICTED_KEY" \
--projectId="$INFISICAL_PROJECT_ID" --env=prod
| 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) |
# 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.
If the grace period was set to >0, the old key auto-expires at the deadline — no action needed. To explicitly expire earlier:
Verify:
curl -sS -o /dev/null -w "%{http_code}\n" -u "$OLD_RESTRICTED_KEY:" https://api.stripe.com/v1/customers?limit=1
# Expect: 401
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": "..."
}
If validation fails before the grace period ends, revert to the old key:
heroku config:set STRIPE_RESTRICTED_KEY="$OLD_KEY" -a raxx-api-prod.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.