Mode: operator-assisted Last validated: 2026-04-24 UTC Validation method: read-only-docs Average duration: 6m Required role: ops
Applies to: POSTMARK_SERVER_TOKEN (per-server API token used by Raptor for transactional email). Account-level Postmark tokens (used to create new servers) are not in scope here — they are rotated via Postmark Account → API Tokens by the superadmin.
Postmark allows up to 3 active API tokens per server. Use this for atomic swaps: create the new token first, propagate, then delete the old. No grace-period setting like Stripe — multi-token coexistence is the rotation pattern.
https://account.postmarkapp.com/# Confirm the current token works
curl -sS -H "X-Postmark-Server-Token: $CURRENT_TOKEN" \
https://api.postmarkapp.com/server | jq '.Name'
# Expect: server name (e.g., "raxx-prod")
Postmark does not expose a programmatic API for token rotation; the dashboard is the only path.
https://account.postmarkapp.com/ and select the Server to rotate.NEW_TOKEN="..."
curl -sS -H "X-Postmark-Server-Token: $NEW_TOKEN" \
https://api.postmarkapp.com/server | jq '{Name, ID, ApiTokens}'
# Expect: same Name and ID as step 1, ApiTokens count increased by 1
Send a test email through the new token (use a deliverable address you control):
curl -sS -X POST -H "X-Postmark-Server-Token: $NEW_TOKEN" \
-H "Content-Type: application/json" \
https://api.postmarkapp.com/email \
--data '{
"From":"noreply@raxx.app",
"To":"kris@moosequest.net",
"Subject":"Token rotation validation 2026-04-24",
"TextBody":"This email confirms POSTMARK_SERVER_TOKEN rotation validation."
}' | jq '.MessageID'
# Expect: a UUID. Confirm receipt in inbox.
infisical secrets set POSTMARK_SERVER_TOKEN="$NEW_TOKEN" \
--projectId="$INFISICAL_PROJECT_ID" --env=prod
| Consumer | How |
|---|---|
| Raptor (raxx-api-prod, raxx-api-staging) | heroku config:set POSTMARK_SERVER_TOKEN="$NEW_TOKEN" -a raxx-api-prod (and -staging) |
| Notification service (if separate) | Update its config var |
| GitHub Actions (only if email tests are run in CI — usually not) | gh secret set POSTMARK_SERVER_TOKEN -b ... |
After dyno restart:
# Trigger a real (or test-flagged) transactional email through Raptor
curl -sS -X POST https://api.raxx.app/api/notifications/test-email \
-H "Content-Type: application/json" \
-d '{"to":"kris@moosequest.net","subject":"rotation validation"}'
# Confirm receipt in inbox; confirm the Raptor logs show MessageID returned by Postmark
heroku logs --tail -a raxx-api-prod | grep -i postmark
# Expect: no "401" / "InvalidAPIToken" entries
Verify:
curl -sS -o /dev/null -w "%{http_code}\n" \
-H "X-Postmark-Server-Token: $OLD_TOKEN" \
https://api.postmarkapp.com/server
# Expect: 401
action: secret.rotate.completed
actor: <admin_id>
context: {
"secret_name": "POSTMARK_SERVER_TOKEN",
"server_name": "raxx-prod",
"method": "operator-assisted-dashboard"
}
Until step 7, both old and new tokens are valid. To roll back:
heroku ps:restart -a raxx-api-prod).After step 7 (deletion), the old token is gone. If the new token is broken at that point, generate a fresh new token via the dashboard (you now have 2 token slots — the rotated-but-broken one, and the freshly generated working one), validate, propagate, then delete the broken one.