Rotation SOP — Postmark Server API Token
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.
When to run
- Scheduled rotation (cadence: every 180 days — transactional email is moderate-risk)
- Operator-initiated (suspected compromise, off-cycle)
- After incident (employee offboarding)
Prerequisites
- [ ] Postmark dashboard access via SSO/email at
https://account.postmarkapp.com/ - [ ] Server name known (e.g., "raxx-prod", "raxx-staging")
- [ ] Existing token in Infisical with history
- [ ] Downstream consumer list: Raptor email senders (welcome, billing, password reset), notification service
- [ ] Aware: server has a max of 3 simultaneous API tokens — if at the limit, delete an unused token first
Steps
1. Pre-rotation checks
# 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")
2. Generate the new credential
Postmark does not expose a programmatic API for token rotation; the dashboard is the only path.
- Navigate to
https://account.postmarkapp.com/and select the Server to rotate. - Click the API Tokens tab.
- Click "Generate New".
- Optionally add a name/description (recommended: "rotation 2026-04-24").
- Copy the new token value immediately — shown once.
- Do NOT delete the old token yet (that's step 7).
3. Validate the new credential
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.
4. Store in Infisical
infisical secrets set POSTMARK_SERVER_TOKEN="$NEW_TOKEN" \
--projectId="$INFISICAL_PROJECT_ID" --env=prod
5. Propagate to downstream consumers
| 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 ... |
6. Verify downstream
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
7. Revoke the old credential
- Postmark dashboard → Server → API Tokens.
- Locate the OLD token.
- Click Delete (or the trash icon — UI varies).
- Confirm.
Verify:
curl -sS -o /dev/null -w "%{http_code}\n" \
-H "X-Postmark-Server-Token: $OLD_TOKEN" \
https://api.postmarkapp.com/server
# Expect: 401
8. Audit log entry
action: secret.rotate.completed
actor: <admin_id>
context: {
"secret_name": "POSTMARK_SERVER_TOKEN",
"server_name": "raxx-prod",
"method": "operator-assisted-dashboard"
}
Rollback
Until step 7, both old and new tokens are valid. To roll back:
- Revert Heroku config vars to the OLD token (from Infisical history).
- Restart dynos (
heroku ps:restart -a raxx-api-prod). - Skip step 7. Investigate the new token's issue.
- After diagnosis, rotate fresh from step 2.
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.
Vendor doc references
- How to cycle a server token: https://postmarkapp.com/support/article/1293-how-to-cycle-a-server-api-token
- Token types: https://postmarkapp.com/support/article/1008-what-are-the-account-and-server-api-tokens
- Server API: https://postmarkapp.com/developer/api/server-api
Known gotchas
- Max 3 active tokens per server. Delete unused tokens before generating a 4th.
- No grace period UI like Stripe. Coexistence is achieved by not deleting the old one until propagation is verified.
- No programmatic rotation API. Dashboard only.
- SMTP tokens are separate. Postmark also issues SMTP credentials; those are managed under each Outbound Stream's Settings tab and rotate independently. This SOP does not cover them.
- Account-level tokens are different. The Account API Token (used to create servers) lives at Account → API Tokens. Use a separate rotation procedure if/when that token is in scope.
- Test emails count toward the server's send quota. Use a low-volume server or a test recipient you control.