Mode: operator-assisted Last validated: 2026-04-24 UTC Validation method: read-only-docs (dry-run-on-prod-with-rollback feasible — see below) Average duration: 8m Required role: superadmin
Applies to: HEROKU_API_KEY, HEROKU_PLATFORM_API_TOKEN, and any other long-lived Heroku Platform API authorization tokens used by the console, the rotation pipeline, and CI workflows.
Earlier verbal summary said "manual." Confirmed: there is no programmatic self-rotation API. Heroku exposes
heroku authorizations:create(CLI) andPOST/DELETE /oauth/authorizations(REST) — but creating a new authorization requires a user-level OAuth token or the operator's authenticated CLI session, which itself cannot be rotated programmatically without operator presence. We classify this as operator-assisted (operator runs CLI commands) rather than fully manual (dashboard-only).
heroku login) on a trusted workstation# List existing authorizations
heroku authorizations
# Look for the description matching the token being rotated (e.g., "raxx-api-prod-platform-token")
# Capture the OLD authorization ID for revocation in step 7
# Create a new long-lived authorization
heroku authorizations:create \
--description "raxx-platform-token-2026-04-24" \
--scope "global"
Output includes the new token value (Token) and the authorization ID. Capture the token value now — it is shown once.
For a more restricted scope (preferred if the consumer doesn't need global access):
heroku authorizations:create \
--description "raxx-platform-token-2026-04-24-readonly" \
--scope "read-protected,write-protected"
REST equivalent (if CLI unavailable):
curl -sS -n -X POST \
-H "Accept: application/vnd.heroku+json; version=3" \
-H "Content-Type: application/json" \
https://api.heroku.com/oauth/authorizations \
--data '{"description":"raxx-platform-token-2026-04-24","scope":["global"]}'
NEW_TOKEN="<captured value>"
curl -sS -H "Accept: application/vnd.heroku+json; version=3" \
-H "Authorization: Bearer $NEW_TOKEN" \
https://api.heroku.com/account | jq '.email'
# Expect: kris@moosequest.net (or the Heroku account email)
Confirm the token can perform the actions consumers need:
# Read app config (used by rotation pipeline + console)
curl -sS -H "Accept: application/vnd.heroku+json; version=3" \
-H "Authorization: Bearer $NEW_TOKEN" \
https://api.heroku.com/apps/raxx-api-prod/config-vars | jq 'keys | length'
# Expect: a non-zero count.
infisical secrets set HEROKU_API_KEY="$NEW_TOKEN" \
--projectId="$INFISICAL_PROJECT_ID" --env=prod
| Consumer | How |
|---|---|
| Raptor (raxx-api-prod, raxx-api-staging) | heroku config:set HEROKU_API_KEY="$NEW_TOKEN" -a raxx-api-prod (and -staging) |
| Console (raxx-console-prod) | heroku config:set HEROKU_API_KEY="$NEW_TOKEN" -a raxx-console-prod |
| GitHub Actions (deploy workflows) | gh secret set HEROKU_API_KEY -a actions -b "$NEW_TOKEN" |
| Rotation pipeline | per #253 deploy spec — likely a Heroku config var on the pipeline app |
| Operator local zshrc | DM via Slack D0AJ7K184TV after Infisical sync |
After Heroku propagation completes (each config:set triggers a dyno restart):
# Confirm Raptor is up and using the new token (health endpoint hits Heroku Platform API on a /health/dependencies route if present)
curl -sS https://api.raxx.app/health | jq '.heroku_api'
# OR a deploy-via-token smoke test on staging
gh workflow run deploy-staging.yml -f sanity=true
gh run watch
# Expect: success
heroku authorizations:revoke <OLD_AUTHORIZATION_ID>
REST equivalent:
curl -sS -X DELETE \
-H "Accept: application/vnd.heroku+json; version=3" \
-H "Authorization: Bearer $ROTATING_TOKEN" \
https://api.heroku.com/oauth/authorizations/$OLD_AUTHORIZATION_ID
Verify:
curl -sS -H "Accept: application/vnd.heroku+json; version=3" \
-H "Authorization: Bearer $OLD_TOKEN" \
https://api.heroku.com/account
# Expect: HTTP 401
action: secret.rotate.completed
actor: <admin_id>
context: {"secret_name": "HEROKU_API_KEY", "old_authorization_id": "<id>", "new_authorization_id": "<id>", "method": "operator-assisted"}
Heroku permits multiple active authorizations. Until step 7 completes, the old authorization is still valid:
heroku ps:restart -a raxx-api-prod) to pick up the reverted token.After step 7 (revocation), the old token cannot be restored — Heroku does not retain revoked authorizations.
heroku authorizations)heroku authorizations:create requires an authenticated CLI session, which is itself a token. We cannot rotate Heroku tokens 100% unattended; an operator must run the create command (or a CI job using a separate, manually-rotated, longer-lived management token).config:set triggers dyno restart. Plan for ~30s of dyno cycle per app. Use heroku config:set with multiple keys at once to batch restarts.HEROKU_API_KEY) is the secret string; the authorization ID is the management handle for revocation. Capture both at creation.heroku authorizations:rotate does not exist. Despite intuition, the Heroku CLI does not have a one-shot rotate. Use create-then-revoke explicitly.