Raxx · internal docs

internal · gated ↑ index

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

Prerequisites

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.

  1. Navigate to https://account.postmarkapp.com/ and select the Server to rotate.
  2. Click the API Tokens tab.
  3. Click "Generate New".
  4. Optionally add a name/description (recommended: "rotation 2026-04-24").
  5. Copy the new token value immediately — shown once.
  6. 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

  1. Postmark dashboard → Server → API Tokens.
  2. Locate the OLD token.
  3. Click Delete (or the trash icon — UI varies).
  4. 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:

  1. Revert Heroku config vars to the OLD token (from Infisical history).
  2. Restart dynos (heroku ps:restart -a raxx-api-prod).
  3. Skip step 7. Investigate the new token's issue.
  4. 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

Known gotchas