Raxx · internal docs

internal · gated

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:

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

  1. Log in to the FreeScout admin panel.
  2. Navigate to Manage → Apps and find the Webhooks app (install if not present).
  3. Click New Webhook and configure: - Payload URL: https://api.raxx.app/api/internal/freescout-webhook/audit (use https://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 all Conversation events (status changed, created, updated). The handler no-ops on unknown events, so a broad subscription is safe.
  4. 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:

  1. Generate a new secret (Step 1 above).
  2. Update Infisical at /MooseQuest/freescout/ FREESCOUT_AUDIT_WEBHOOK_SECRET.
  3. Restart dynos to pick up the new vault value: heroku ps:restart --app raxx-api-staging >/dev/null 2>&1
  4. Immediately update the FreeScout webhook secret in admin UI (same session).
  5. 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:

  1. 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>';
  2. 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).
  3. 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.