FreeScout Postmark SMTP relay — operator runbook
Issue: #669 (epic #651)
Component: FreeScout outbound email, Postmark relay, delivery monitor
Last reviewed: 2026-05-04
Flag: FLAG_POSTMARK_DELIVERY_MONITOR (default off)
Overview
FreeScout (at tickets.raxx.app) routes all outbound email through Postmark as
an SMTP relay. This avoids Lightsail's blocked port 25 + shared-IP reputation
problems. A delivery monitor webhook persists every Delivery/Bounce/SpamComplaint
event and alerts the operator via Slack DM when threshold rates are exceeded.
Part 1 — FreeScout SMTP configuration (operator UI)
These steps are performed in the FreeScout admin UI, not via code. Do this once
after the Postmark Server is provisioned and POSTMARK_SERVER_API_KEY is in vault.
Step 1.1 — Retrieve the Postmark server token
The token is stored in Infisical vault at:
path: /MooseQuest/postmark/
key: POSTMARK_SERVER_API_KEY
env: prod
Do NOT use POSTMARK_ACCOUNT_API_KEY — that is the account-level key and cannot
be used as an SMTP credential.
Step 1.2 — Configure FreeScout SMTP outbound
- Sign into
https://tickets.raxx.app/(CF Access gate — usekris@moosequest.net). - Navigate to Admin → Manage → Settings → General.
- Scroll to the Email Sending (SMTP) section.
- Set:
| Field | Value |
|---|---|
| SMTP Host | smtp.postmarkapp.com |
| SMTP Port | 587 |
| Encryption | STARTTLS |
| SMTP Username | (the POSTMARK_SERVER_API_KEY value from vault) |
| SMTP Password | (same as username — Postmark uses the token as both) |
| From Name | Raxx Support |
| From Email | support@raxx.app |
- Click Save.
- Use the Send Test Email button on the same page.
- Enter your own email address and click send.
- Within 30 seconds, verify the email arrives in your inbox with correct
DKIM/SPF pass markers (check "Show original" in Gmail to see the
Authentication-Resultsheader).
Step 1.3 — Verify DKIM/SPF in Postmark dashboard
- In Postmark: Servers → raxx → Settings → Signatures.
- The
raxx.appdomain signature should show all three statuses green: - SPF:Verified- DKIM:Verified- Return-Path:Verified
If any show red, the DNS records from PR #676 may not have propagated yet. Wait 30 minutes and refresh.
Part 2 — Postmark delivery webhook setup
The delivery monitor receives Postmark events via webhook. Set up the webhook in the Postmark dashboard after the code (PR #669) is deployed.
Step 2.1 — Generate the webhook secret
Generate a random secret to authenticate Postmark deliveries:
python3 -c "import secrets; print(secrets.token_urlsafe(32))"
Step 2.2 — Set the secret in Heroku
heroku config:set POSTMARK_DELIVERY_WEBHOOK_SECRET=<generated-value> \
--app raxx-api-prod >/dev/null 2>&1
Also set it on staging:
heroku config:set POSTMARK_DELIVERY_WEBHOOK_SECRET=<same-value> \
--app raxx-api-staging >/dev/null 2>&1
Store the value in vault:
path: /MooseQuest/postmark/
key: POSTMARK_DELIVERY_WEBHOOK_SECRET
env: prod
Step 2.3 — Configure the webhook in Postmark
- In Postmark: Servers → (your server) → Settings → Outbound → Webhooks.
- Click Add webhook.
- Set:
| Field | Value |
|---|---|
| Webhook URL | https://api.raxx.app/webhooks/postmark/delivery |
| Auth header | X-Postmark-Webhook-Token: <value from step 2.1> |
| Events | Delivery, Bounce, SpamComplaint |
- Click Test — you should see
HTTP 401because the flag is still off. That is correct; once the flag is on, it will returnHTTP 200.
Step 2.4 — Enable the feature flag
After deployment and webhook secret is set:
heroku config:set FLAG_POSTMARK_DELIVERY_MONITOR=1 \
--app raxx-api-prod >/dev/null 2>&1
Test the webhook in Postmark dashboard again — it should now return HTTP 200.
Part 3 — Slack DM alerts (optional)
When SLACK_BOT_TOKEN is set, the delivery monitor sends operator DMs when
bounce or spam complaint rates exceed thresholds.
| Threshold | Rate | Window |
|---|---|---|
| Bounce alert | >1% | Last 1 hour |
| Spam alert | >0.1% | Last 24 hours |
To enable:
# Get the bot token from vault at /MooseQuest/slack/SLACK_BOT_TOKEN
heroku config:set SLACK_BOT_TOKEN=<xoxb-token> --app raxx-api-prod >/dev/null 2>&1
The DM channel defaults to D0AJ7K184TV (operator DM). Override:
heroku config:set SLACK_OPERATOR_DM_CHANNEL=D0AJ7K184TV \
--app raxx-api-prod >/dev/null 2>&1
Note: Slack alerts are non-fatal. A misconfigured or absent Slack token never blocks the webhook response or the mail flow.
Part 4 — Recent deliveries API
Once the flag is on, the recent-deliveries endpoint is available at:
GET https://api.raxx.app/api/_internal/postmark/recent-deliveries
Response shape:
{
"events": [
{
"id": 42,
"event_type": "Delivery",
"message_id": "abc-123",
"recipient": "user@example.com",
"tag": "support-outbound",
"bounce_type": null,
"bounce_description": null,
"recorded_at": "2026-05-04 12:34:56"
}
],
"counts_24h": {
"delivered": 150,
"bounced": 2,
"spam_complaint": 0
},
"total_24h": 152
}
This endpoint is behind CF Access (api.raxx.app) — no additional auth
required for operator access.
Part 5 — Token rotation via Velvet (#950)
When the Postmark sender-token adapter (#950, velvet_postmark_adapter flag) is
stable:
- The
POSTMARK_SERVER_API_KEYrotation will be managed by Velvet. - After Velvet rotates the key, the operator must update FreeScout's SMTP password via the admin UI (Part 1, step 1.2) with the new token value.
- This manual UI step will be automated once a FreeScout API adapter lands (tracked in a follow-up card).
Until #950 is ready, rotation is manual:
- Generate a new Postmark Server API token in the Postmark dashboard.
- Update FreeScout SMTP password (Part 1, step 1.2).
- Update vault at
/MooseQuest/postmark/POSTMARK_SERVER_API_KEY. - Revoke the old token in the Postmark dashboard.
Part 6 — Alerts bell (#789) integration
The delivery monitor is designed to feed the console alerts bell (#789) once that component's alert-source API is stable. Until #789 is fully shipped:
- Threshold crossings are logged at WARNING level in Raptor.
- Slack DMs are sent if
SLACK_BOT_TOKENis configured. - No console bell badge.
When #789 lands, update _check_alert_thresholds() in
backend_v2/api/routes/postmark_delivery.py to also POST to
/api/_internal/alerts/sources/postmark.
Troubleshooting
Webhook returns 401
POSTMARK_DELIVERY_WEBHOOK_SECRETis not set or does not match the value configured in the Postmark dashboard webhook.- Check:
heroku config:get POSTMARK_DELIVERY_WEBHOOK_SECRET --app raxx-api-prod - Verify the Postmark dashboard has the same value under Webhooks → Auth header.
Webhook returns 404
FLAG_POSTMARK_DELIVERY_MONITORis off.- Check:
heroku config:get FLAG_POSTMARK_DELIVERY_MONITOR --app raxx-api-prod - Enable:
heroku config:set FLAG_POSTMARK_DELIVERY_MONITOR=1 --app raxx-api-prod >/dev/null 2>&1
Outbound emails going to spam
- Check Postmark dashboard — Servers → Activity — for delivery failures.
- Check recent-deliveries endpoint for Bounce / SpamComplaint events.
- Verify DKIM/SPF are green in Postmark (Part 1, step 1.3).
- Check SPF record:
dig TXT raxx.app— must includeinclude:spf.mtasv.net. - Check DKIM record:
dig TXT pm._domainkey.raxx.app— must return the Postmark DKIM public key.
FreeScout test email fails
- Confirm SMTP settings match Part 1, step 1.2.
- Check FreeScout error logs on Lightsail:
bash ssh -i /tmp/lightsail_us_east_1.pem admin@54.146.13.200 \ sudo tail -50 /var/log/apache2/freescout-error.log - Test SMTP connectivity from Lightsail to Postmark:
bash ssh -i /tmp/lightsail_us_east_1.pem admin@54.146.13.200 \ "curl -v telnet://smtp.postmarkapp.com:587" 2>&1 | head -20
Vault paths summary
| Key | Path | Env |
|---|---|---|
POSTMARK_SERVER_API_KEY |
/MooseQuest/postmark/ |
prod |
POSTMARK_DELIVERY_WEBHOOK_SECRET |
/MooseQuest/postmark/ |
prod |
POSTMARK_ACCOUNT_API_KEY |
/MooseQuest/postmark/ |
prod |
SLACK_BOT_TOKEN |
/MooseQuest/slack/ |
prod |
Environment variables summary
| Variable | Required | Description |
|---|---|---|
POSTMARK_DELIVERY_WEBHOOK_SECRET |
Yes (for flag on) | Shared token from Postmark webhook config |
FLAG_POSTMARK_DELIVERY_MONITOR |
Yes (must be 1) |
Feature flag to enable endpoints |
SLACK_BOT_TOKEN |
Optional | Slack bot token for delivery alerts |
SLACK_OPERATOR_DM_CHANNEL |
Optional | DM channel (default D0AJ7K184TV) |