FreeScout Audit Webhook Setup
Endpoint: POST /api/internal/freescout-webhook/audit
Feature flag: FLAG_FREESCOUT_WEBHOOK_RECEIVE (default OFF)
Issue: #1484 (SC-A4)
Purpose
This webhook keeps freescout_ticket_cache current within 5 seconds of a
FreeScout ticket status change. The audit writer (SC-A5) reads from this
cache synchronously to determine ticket_state_at_read for
operator_interaction audit events.
Cache miss = fail-closed = ticket_state_at_read='none' = Path B (security
incident notification). Keeping this webhook healthy is operationally important.
Pre-flight checklist
Before enabling FLAG_FREESCOUT_WEBHOOK_RECEIVE=1:
- [ ] Migration
016_unified_audit_v2_schema.sqlapplied andfreescout_ticket_cachetable exists in the target database. - [ ]
FREESCOUT_AUDIT_WEBHOOK_SECRETset in Infisical at path/MooseQuest/freescout/with keyFREESCOUT_AUDIT_WEBHOOK_SECRET. The Infisical bootstrap script injects this as an env var at dyno start. Verify:heroku config --app raxx-api-staging | grep FREESCOUT_AUDITshould NOT show it (it comes from vault, not Heroku config). - [ ] Webhook registered in FreeScout admin UI (see step below).
- [ ] Staging soak: enable on
raxx-api-stagingfirst; verifyfreescout_ticket_cachepopulates; then promote to prod.
Step 1: Generate the HMAC secret
python3 -c "import secrets; print(secrets.token_urlsafe(48))"
Store the output in Infisical at:
- Path: /MooseQuest/freescout/
- Key: FREESCOUT_AUDIT_WEBHOOK_SECRET
Do not set this via heroku config:set — vault is the source of truth.
Step 2: Register the webhook in FreeScout admin UI
- Log in to the FreeScout admin panel.
- Navigate to Manage → Apps and find the Webhooks app (install if not present).
- Click New Webhook and configure:
- Payload URL:
https://api.raxx.app/api/internal/freescout-webhook/audit(usehttps://api-staging.raxx.app/...for staging) - Content Type:application/json- Secret: the HMAC secret from Infisical (paste once; FreeScout stores its own copy) - Events: select allConversationevents (status changed, created, updated). The handler no-ops on unknown events, so a broad subscription is safe. - Save and note the webhook ID.
The URL ...freescout-webhook/audit is the audit cache path.
The RBAC auto-revocation webhook (RV-4, future) will use
...freescout-webhook/rbac — register separately when that ships.
Step 3: Enable the feature flag
Staging:
heroku config:set FLAG_FREESCOUT_WEBHOOK_RECEIVE=1 --app raxx-api-staging >/dev/null 2>&1
Prod (after 48-hour staging soak):
heroku config:set FLAG_FREESCOUT_WEBHOOK_RECEIVE=1 --app raxx-api-prod >/dev/null 2>&1
Step 4: Verify delivery
After registering the webhook, change a ticket status in FreeScout (e.g., close a test ticket). Then query the cache:
SELECT ticket_id, status, updated_at, ttl_expires
FROM freescout_ticket_cache
ORDER BY updated_at DESC
LIMIT 10;
The row should appear within ~5 seconds of the status change. If it doesn't:
1. Check Heroku logs: heroku logs --tail --app raxx-api-staging | grep freescout_audit
2. Verify FreeScout webhook delivery log (Manage → Apps → Webhooks → delivery history).
3. Confirm FREESCOUT_AUDIT_WEBHOOK_SECRET is correctly set in Infisical and
propagated to the dyno.
HMAC secret rotation
Rotating the HMAC secret requires atomic update of both Infisical and FreeScout admin UI:
- Generate a new secret (Step 1 above).
- Update Infisical at
/MooseQuest/freescout/FREESCOUT_AUDIT_WEBHOOK_SECRET. - Restart dynos to pick up the new vault value:
heroku ps:restart --app raxx-api-staging >/dev/null 2>&1 - Immediately update the FreeScout webhook secret in admin UI (same session).
- Verify delivery (Step 4) before considering the rotation complete.
If webhook deliveries fail during rotation (brief window): FreeScout will
retry. The freescout_ticket_cache may be briefly stale; this is acceptable
during planned rotation. Document the rotation window in the ops channel.
Polling fallback (post-launch, if needed)
If webhook delivery proves unreliable (e.g., during FreeScout restarts or network partitions), a polling fallback can be activated separately. The polling interval should be ≤5 minutes to meet the 5-second-at-best SLA in steady state. See SC-A4 issue #1484 Phase 2+ for implementation.
Webhook delivery gaps
FreeScout may miss delivering a webhook during a restart or network partition. If you suspect a gap:
- Query the cache for tickets known to have changed status:
sql SELECT ticket_id, status, updated_at FROM freescout_ticket_cache WHERE updated_at < '<gap_start_utc>'; - For any stale rows, manually re-trigger by updating the ticket status in FreeScout admin UI (a no-op status change triggers a fresh webhook).
- The polling fallback (if activated) handles this automatically.
Endpoint path coordination with RV-4
Both this card (SC-A4) and the RBAC auto-revocation card (RV-4) receive FreeScout events. They use separate endpoint paths to avoid routing collisions:
| Purpose | Path |
|---|---|
| Audit cache | /api/internal/freescout-webhook/audit |
| RBAC revoke | /api/internal/freescout-webhook/rbac |
Both paths share the same HMAC validation helper
(api.routes.freescout_audit_webhook.verify_freescout_hmac). If RV-4 uses
a separate HMAC secret, update its registration separately.