Enterprise-Grade Webhook Router & ConnectWise Bridge
HookWise is a high-performance webhook router that connects monitoring and security sources such as Uptime Kuma, Zabbix, Grafana, Datadog, and CIPP to ConnectWise Manage tickets. It provides company-aware duplicate detection, durable asynchronous delivery, optional local AI analysis, and configuration/asset association.
- Quick Start
- Architecture & Flow
- Advanced Features
- Web Console
- API Reference
- HMAC Security & Verification
- AI In-Depth
- Extensive Configuration
- Deep-Dive Usage
- Configuration Recipes
- Dynamic Company Identification
- ConnectWise Configuration Auto-Linking
- Troubleshooting & FAQ
- Identity & Access (RBAC + Entra ID)
- Security & Compliance
- Development & Contributing
- License
Requirements: Docker Engine with Docker Compose and ConnectWise Manage API credentials.
-
Copy
.env.exampleto.envand replace every sample credential. Add a uniquePOSTGRES_PASSWORD; production also requires strongSECRET_KEY,GUI_PASSWORD,REDIS_PASSWORD, and valid FernetENCRYPTION_KEYvalues. -
Review the selected Compose file before starting.
CW_URL,CW_COMPANY,CW_PUBLIC_KEY,CW_CLIENT_ID, the default ticket settings, and several proxy/TLS settings are currently literal values indocker-compose.ymlanddocker-compose.ghcr.yml; edit or override them for your environment. -
Start either a local source build or the published GHCR image:
# Build from this checkout docker compose up -d --build # Or use the published image docker compose -f docker-compose.ghcr.yml up -d --pull always
-
For local HTTP-only evaluation, add
SESSION_COOKIE_SECURE=falseto the shared Compose environment, then openhttp://localhost:5000and sign in asadminwith the configuredGUI_PASSWORD. The one-shothookwise-migrateservice applies database migrations and bootstraps this account before the application services start.
Never deploy the sample passwords or keys. For production, place HookWise behind an HTTPS reverse proxy and keep secure session cookies enabled. If AI RCA is enabled, pull the default model once with docker exec hookwise-llm ollama pull qwen3.5:4b. See the operator runbook for health checks, recovery, secret rotation, and DLQ procedures.
HookWise uses a distributed architecture to ensure reliability and low-latency webhook ingestion.
graph TD
Client[Monitoring Source] -->|HTTPS Webhook| Proxy[Flask / Gevent Proxy]
Proxy -->|Commit log + delivery intent| DB[(PostgreSQL)]
Proxy -->|Dispatch task| Redis[(Redis Broker)]
DB -->|Recover pending intents| Outbox[Outbox Dispatcher]
Outbox -->|Retry dispatch| Redis
Redis -->|Process| Worker[Celery Worker]
Worker -.->|Optional RCA| AI[Ollama / Qwen3.5 4B]
Worker -->|PSA API| CW[ConnectWise Manage]
Worker -->|Status + diagnostics| DB
Proxy -->|Live Feed| GUI[Web GUI / Socket.io]
- Ingestion: The proxy validates the endpoint, source IP, configured authentication, payload size, and rate limit.
- Durable staging: A request ID, history row, and delivery intent are committed together in PostgreSQL.
- Queuing: The delivery is dispatched to Redis/Celery; an outbox dispatcher recovers a committed intent if the broker was unavailable.
- Resolution: The worker applies JSONPath mappings, regex routing rules, maintenance windows, and final company selection.
- Deduplication: HookWise checks its endpoint/company/summary cache and revalidates a matching open ticket within the resolved ConnectWise company as needed.
- Action: The ticket is created, updated, or closed in ConnectWise.
- Asset association: When enabled, HookWise attaches one exact, active ConnectWise configuration belonging to the assigned company.
- AI insights: For new tickets on endpoints with AI RCA enabled, Ollama generates an internal RCA note.
flowchart TD
A[Start Process] --> B{Existing Open Ticket?}
B -- Yes --> C{Status in Payload?}
C -- "Close Value" --> D[Close Ticket]
C -- Other --> E[Add Internal Note]
B -- No --> F{Status in Payload?}
F -- "Open Value" --> G[Create New Ticket]
F -- Other --> H[Skip/Log Only]
G --> I{AI RCA Enabled?}
I -- Yes --> J[Analyze with AI]
J --> K[Add Internal RCA Note]
I -- No --> L[Finish]
K --> L
flowchart TD
A[Incoming Webhook] --> B{Global Maintenance?}
B -- Yes --> C[Log as Skipped]
B -- No --> D{Window Matches?}
D -- "Daily Schedule" --> C
D -- "Weekly Day" --> C
D -- No Match --> E[Process Normally]
sequenceDiagram
participant S as Monitoring Source
participant P as Flask Proxy
participant D as PostgreSQL DB / Outbox
participant R as Redis Broker
participant W as Celery Worker
participant A as Ollama (Qwen3.5 4B)
participant C as ConnectWise API
participant O as Outbox Recovery
S->>P: POST /w/<id> (configured auth)
P->>P: Validate source, auth, size, and rate
P->>D: Commit history + delivery intent
P->>R: Dispatch delivery task
alt Broker accepts the task
P-->>S: 202 Accepted (request ID)
else Broker unavailable
P-->>S: 503 (delivery retained)
O->>D: Load pending delivery
O->>R: Retry dispatch
end
R->>W: Fetch Task
W->>C: Find company-scoped open ticket
alt Exists
W->>C: Update Ticket / Add Note
else New
W->>C: Create Ticket
opt AI RCA enabled
W->>A: Analyze Payload
A-->>W: Root Cause Note
W->>C: Add Internal RCA Note
end
end
opt Configuration auto-link enabled
W->>C: Attach unique company configuration
end
W->>D: Persist Final Status & Logs
- Regex Rule Engine: Route
CRITICALalerts to the "Emergency" board andWARNalerts to "Tiling" automatically. - Smart Maintenance: Define recurring maintenance windows (Daily, Weekly, Once) with support for overnight schedules (e.g., 22:00 to 04:00) using UTC-normalized logic.
- Company Mapping: Supports
#CW<ID>in titles or dynamic lookups from payload fields. - Webhook Timeout Alerts (Heartbeat): Automatically trigger a ticket if an endpoint hasn't received data within a configured threshold (e.g., "No data for 24h"). Alerts repeat at the same hourly interval if the endpoint remains stale, adding a note to the existing ticket or creating a new one if it was closed. The alert state resets as soon as the next webhook arrives.
- Transactional outbox: HookWise commits the webhook history row and dispatch intent together. If Redis is unavailable, the request returns
503, but the committed delivery remains available for automatic outbox recovery. - Per-endpoint controls: Configure an ingress limit, bounded retry count, initial delay, and maximum delay. Retryable failures use jittered exponential backoff.
- Dead-letter queue: Exhausted or non-retryable deliveries enter the DLQ with attempt lineage and error context for operator review and replay.
- Failure thresholds: Optionally notify the configured system health webhook when an endpoint reaches a defined number of failures within a time window.
HookWise can generate automated troubleshooting guides using local LLMs. It analyzes the raw payload and adds an internal note to the ticket with:
- Potential root causes.
- Suggested troubleshooting steps.
- Technical summary of the alert.
Managing the Model:
By default, HookWise uses qwen3.5:4b. Pull or update it manually with:
docker exec -it hookwise-llm ollama pull qwen3.5:4b- Live Activity Hub: Filterable real-time Socket.IO feed with pause/resume, bounded buffers, duplicate suppression, persistent annotations, and reconnect handling.
- Safe lifecycle controls: Endpoint archive/restore is reversible and CSRF-protected; restored endpoints remain paused until reviewed.
- Audit trail: Key endpoint, identity, access-control, and administrative mutations are recorded with the actor and timestamp.
- Metrics and health: Authenticated Prometheus metrics plus public container probes and authenticated dependency diagnostics.
The dashboard provides drill-down KPIs, comparison deltas, endpoint activity, and timezone-aware event charts with failure rates and P50/P95/P99 latency. Operators can select preset or custom time ranges, refresh manually or automatically, hide and reorder KPIs, and use compact mode. Layout, timezone, refresh interval, and activity-buffer preferences are stored per user.
- Search and filter by name, URL, board, company, health, state, tags, or the safe last four characters of a token. Switch between list and grid views, sort/group results, and pin or drag endpoints into priority order.
- Create drafts, clone configurations, queue a realistic test webhook, preview routing without calling ConnectWise, rotate bearer tokens, and import or export endpoint JSON. Single-endpoint exports omit bearer and HMAC secrets; imports receive a fresh ID/token and start paused.
- Apply bulk board/priority changes, pause, resume, archive, or export selected endpoints. Archived endpoints preserve their configuration and history and can be restored.
- The editor progressively hydrates ConnectWise board/status/type data while preserving saved selections. Unsaved non-secret form fields are restored locally after a reload; secret, file, and hidden fields are never placed in that draft cache.
History includes basic and advanced filters, saved searches, live tail, retry/DLQ state, ConnectWise quota and endpoint rate-limit context, and bulk actions. Secret-safe diagnostics show the processing timeline, error chain, and retry attempts without including stored payloads or request headers in downloads. Failed deliveries can be retried, replayed with an edited JSON payload, or replayed from the DLQ in batches of up to 50.
Settings covers retention, system health notifications, CIPP exclusions, ConnectWise lookup-cache refresh, LLM diagnostics, and local TOTP setup. Configuration backup/restore produces an encrypted, authenticated file containing endpoints (including delivery secrets), tags, tenant mappings, and user dashboard preferences.
Important
A configuration backup is not a PostgreSQL, webhook-history, audit-log, or user-account backup. Restoring it on another installation requires the same ENCRYPTION_KEY; protect that key separately. Uploads are limited to 5 MiB, and the web-console restore action requires settings:write.
GUI and administrative APIs require an authenticated browser session or configured HTTP Basic Auth. Browser sessions are subject to RBAC, and state-changing browser requests are CSRF-protected. Basic Auth is intended for trusted headless clients, must name an active HookWise account, and should be restricted with GUI_TRUSTED_IPS.
Caution
Treat the configured Basic Auth credential as privileged automation access, not as a least-privilege replacement for an interactive RBAC session.
Webhook authentication is configured per endpoint: Bearer and HMAC may be used independently or together; when both are configured, both must validate. Disabling both is rejected unless Explicitly allow unauthenticated delivery is enabled. Trusted-IP/CIDR restrictions and per-endpoint rate limits apply independently of the authentication mode.
POST /w/<endpoint_id>- Auth: Configured Bearer token and/or HMAC signature; unauthenticated ingestion must be explicitly opted into.
- Returns:
202 Acceptedwithrequest_idafter dispatch, validation/authentication errors as4xx, or503when broker dispatch failed and the delivery was retained in the durable outbox.
- Dashboard:
GET /api/stats,GET /api/stats/history,GET /api/dashboard/overview,GET /api/dashboard/analytics, andGET|PATCH|DELETE /api/dashboard/preferences. - Endpoint telemetry:
GET /api/endpoints/summary, including optional secret-safe token-suffix matching. - Activity:
GET /api/activity/streamandPUT|DELETE /api/activity/events/<log_id>/annotation. - History:
GET /api/history/advanced,GET|POST /api/history/saved-searches,GET /api/history/<log_id>/diagnostics,POST /api/history/<log_id>/retry,POST /api/history/<log_id>/replay-edits,POST /api/history/dlq/replay, andGET /api/history/operations. - ConnectWise lookups: Cached board, priority, status, type, subtype, item, and company lists under
/api/cw/*. - Endpoint validation:
POST /endpoint/test/<config_id>queues a real test delivery;POST /endpoint/dry-run/<config_id>previews maintenance and routing decisions without calling ConnectWise. - Administration:
POST /admin/maintenance,GET /admin/backup, andPOST /admin/restore.
GET /healthandGET /readyzare deliberately unauthenticated container probes.GET /health/servicesreports Redis, PostgreSQL, and Celery state; browser-session access requiressettings:read.GET /health/llmandGET /api/health/llmreport Ollama health; browser-session access requiressettings:read.GET /metricsexports Prometheus metrics and requires authentication; use an active Basic Auth account for a headless scraper.
HMAC (Hash-based Message Authentication Code) provides a way to verify both the integrity and the authenticity of a webhook. It ensures that the payload hasn't been tampered with and truly originated from your monitoring tool.
- Shared Secret: You and HookWise share a secret key (configured per endpoint).
- Signing: Sign
<unix_timestamp>.<unique_nonce>.<raw_request_body>with HMAC-SHA256. - Transmission: Send the signature, timestamp, and nonce headers.
- Verification: HookWise verifies the signature, rejects timestamps outside five minutes, and accepts each nonce once.
If your monitoring tool supports custom headers and signing scripts, use the following logic:
1. Calculate the Signature (Python Example):
import hmac
import hashlib
import secrets
import time
secret = "your_hmac_secret_from_gui"
payload = b'{"status": "0", "msg": "Critical Alert"}'
timestamp = str(int(time.time()))
nonce = secrets.token_urlsafe(24)
signed_payload = timestamp.encode() + b"." + nonce.encode() + b"." + payload
signature = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()2. Send the Request:
- Header:
X-HookWise-Signature: <calculated_signature> - Header:
X-HookWise-Timestamp: <unix_timestamp> - Header:
X-HookWise-Nonce: <unique_random_value> - Content-Type:
application/json
Important
Always sign the raw, unformatted body. If your tool beautifies the JSON (adds spaces/newlines) after signing, the verification will fail.
HookWise uses Ollama for optional RCA (Root Cause Analysis). The supplied Compose files point OLLAMA_HOST at the local hookwise-llm container, so no third-party LLM is used by default. If you configure a remote Ollama host, payload-derived prompts leave the HookWise host and must be handled according to your data policy.
By default, HookWise uses qwen3.5:4b. You can swap this for phi4-mini, llama3.2, or another model supported by Ollama:
- Pull the model:
docker exec -it hookwise-llm ollama pull phi4-mini - Update Configuration: Set the
AI_MODELenvironment variable tophi4-mini. - Restart Worker: The Celery worker will now use the new model for all analysis.
AI_MODEL is read by every LLM request. Set it on both proxy and worker deployments when they do not share the Compose environment block.
Qwen3.5 uses reasoning mode by default. HookWise sets LLM_THINK=false so the configured output-token budget is used for the ticket note rather than an internal reasoning trace. For unusually complex alerts, enable thinking and increase LLM_MAX_TOKENS to at least 1536; expect higher CPU latency. LLM_CONTEXT_LENGTH defaults to 4096, which is sufficient for alert payloads while limiting CPU memory usage.
The new-endpoint page includes presets for Uptime Kuma, Zabbix, Grafana, Datadog, and CIPP. A preset only pre-fills routing defaults; review authentication and ConnectWise fields before saving.
Live ticket RCA uses HookWise's concise default system prompt and asks for three likely causes plus three troubleshooting steps. The endpoint editor's RCA Instructions field is currently applied to the built-in LLM dry-run test; live ticket notes continue to use the default prompt.
The LLM_MAX_TOKENS environment variable controls how many tokens Ollama is allowed to generate per RCA response. If your notes appear cut off mid-sentence, this value is too low.
| Value | Expected Output | Best For |
|---|---|---|
100 |
1β2 sentences; often truncated | Smoke tests only |
256 |
Short response; complex alerts may truncate | Strict latency limits |
512 (default) |
Complete, concise RCA for most alerts | Most deployments |
1024 |
More detailed analysis with higher latency | Complex alerts |
2048 |
Very long output with substantially higher latency | Exceptional investigations |
Tip
Start with LLM_MAX_TOKENS=512. Generation time and memory use depend on the model, context length, quantization, and hardware; benchmark changes on the host that runs Ollama.
Note
Token β word. Roughly 1 token β 0.75 words. 512 tokens β ~380 words β enough for a complete, structured RCA note.
| Variable | Usage |
|---|---|
CW_URL |
ConnectWise Manage REST API base URL. |
CW_COMPANY |
Integrator/company identifier used to build ConnectWise authentication. |
CW_PUBLIC_KEY / CW_PRIVATE_KEY |
ConnectWise API member credentials. Treat the private key as a secret. |
CW_CLIENT_ID |
ConnectWise integration client ID. |
CW_DEFAULT_COMPANY_ID |
Fallback ticket company when routing does not resolve a customer. |
CW_TICKET_PREFIX |
Prefix for all summaries (Default: Alert:). |
CW_SERVICE_BOARD |
Primary board if not overridden. |
CW_STATUS_NEW |
Initial status for new tickets. |
CW_STATUS_CLOSED |
Status used when an UP alert is received. |
CW_CONNECT_TIMEOUT / CW_READ_TIMEOUT |
ConnectWise HTTP connect/read timeouts in seconds (Defaults: 5 / 30). |
VIABILITY_TTL |
Seconds a ticket is cached as "open" before re-checking ConnectWise (Default: 300). |
| Variable | Usage |
|---|---|
SECRET_KEY |
Flask session-signing secret. Required outside debug mode. |
ENCRYPTION_KEY |
32-byte Fernet key. DO NOT LOSE. |
RBAC_ENFORCE |
Role enforcement: on (default), log (check + log only), off. See Identity & Access. |
ENTRA_* |
Microsoft Entra ID sign-in β see Identity & Access. |
GUI_USERNAME / GUI_PASSWORD |
Basic-auth credentials for trusted headless clients. The username must belong to an active HookWise account; pair this with GUI_TRUSTED_IPS. |
GUI_TRUSTED_IPS |
Optional global IP/CIDR allowlist for authenticated GUI and administrative routes (e.g., 10.0.0.0/24, 192.168.1.5). |
LOG_RETENTION_DAYS |
Auto-cleanup limit for the webhook_log table. |
SESSION_COOKIE_SECURE |
Send the session cookie only over HTTPS (Default: true outside tests). Disable only for local HTTP development. |
MAX_CONTENT_LENGTH_KB |
Maximum inbound request size before a 413 response (Default: 1024). |
FORCE_HTTPS |
Redirects all traffic to TLS. Requires HTTPS_ORIGIN. |
HTTPS_ORIGIN |
Trusted public HTTPS origin used for redirects (for example, https://hookwise.example.com). |
USE_PROXY / PROXY_FIX_COUNT |
Trust reverse-proxy forwarding headers and set the exact trusted proxy hop count. |
ENABLE_HSTS |
Emit the one-year HSTS header (Default: true; meaningful only over HTTPS). |
CELERY_TASK_SOFT_TIME_LIMIT / CELERY_TASK_TIME_LIMIT |
Worker soft/hard task limits in seconds (Defaults: 120 / 300). |
OLLAMA_HOST |
Ollama API base URL (Compose default: local hookwise-llm). |
LLM_MAX_TOKENS |
Max tokens for LLM RCA responses (Default: 512). Increase if output is truncated. |
LLM_CONTEXT_LENGTH |
Ollama context allocation per request (Default: 4096). Increase only for unusually large payloads. |
LLM_THINK |
Enable Qwen3.5 reasoning before its response (Default: false). Increase the token limit when enabled. |
LLM_TIMEOUT |
Seconds to wait for LLM inference (Default: 900, or 15 minutes). Background tasks and diagnostics include additional shutdown grace. |
See .env.example for the configuration template and the operator runbook for production limits and recovery guidance. Compose interpolates only ${...} entries; edit or override any literal environment values in the selected Compose file.
| Destination | Path Example | Result |
|---|---|---|
| Summary | $.monitor.name |
Extracts Uptime Kuma monitor name. |
| Description | $.msg |
Extracts the alert body. |
| Company | $.tags.client_id |
Maps dynamic client IDs. |
HookWise supports combining multiple JSONPath variables in a single field. Simply space-separate the paths. Empty or null variables in the payload will be automatically ignored. Any segment not starting with $ is treated as literal text.
Note
The field will only be overridden if at least one JSONPath resolves to a non-empty value. Literal-only results are ignored to prevent accidental data loss.
- Example Mapping:
"summary": "$.TaskInfo.Tenant $.TaskInfo.Name" - Payload 1:
{"TaskInfo": {"Tenant": "Acme", "Name": "SRV01"}}-> Result:Acme SRV01 - Payload 2:
{"TaskInfo": {"Name": "SRV01"}}-> Result:SRV01 - Payload 3:
"summary": "Prefix $.SomePath"where$.SomePathis missing -> Result: No Override (Default monitor name is used). - Payload 4:
"summary": "Prefix $.SomePath"where$.SomePathexists -> Result:Prefix Value
Use these in your "Ticket Description Template":
{{ monitor_name }}: The alert source name.{{ msg }}: The alert message.{{ request_id }}: Internal tracking ID.{{ cipp_results }}: Readable English rendering of every item in a CIPPResultsarray.{$..field}: Any valid JSONPath (e.g.,{$..heartbeat.status}).
To suppress certificate-expiry tickets for specific enterprise applications globally, open Settings > General
Configuration and enter one exact name or glob pattern per line under CIPP Certificate Expiry Exclusions.
Matching is case-insensitive and supports * and ? wildcards. Excluded items are removed before formatting. A
webhook containing only excluded applications is recorded as skipped and does not create or update a ConnectWise
ticket.
Example universal CIPP template:
CIPP Alert
Tenant: {$.Tenant}
Alert: {$.TaskInfo.Name}
Source: {$.TaskInfo.Command}
Hookwise Request ID: {{ request_id }}
{{ cipp_results }}
/: Focus Search bar.Esc: Close any open modal.Drag & Drop: Reorder endpoint priority on the dashboard.
Perfect for basic UP/DOWN monitoring.
- Trigger Field:
$.heartbeat.status - Open Value:
0 - Close Value:
1 - JSON Mapping:
{ "summary": "$.monitor.name", "description": "$.heartbeat.msg", "customer_id": "$.monitor.tags.CW_ID" }
For tools that send text-based statuses like "CRITICAL" or "OK".
- Trigger Field:
$.status_text - Open Value:
CRITICAL, WARNING - Close Value:
OK, RESOLVED - Ticket Prefix:
Infrastructure Alert:
Route alerts to different boards based on the hostname.
- Routing Rules:
[ { "path": "$.monitor.hostname", "regex": ".*-DB-.*", "overrides": { "board": "Database Team", "priority": "High" } }, { "path": "$.monitor.hostname", "regex": ".*-FE-.*", "overrides": { "board": "Frontend Team" } } ]
Great for detailed system health and event severity.
- Trigger Field:
$.event.status - Open Value:
PROBLEM - Close Value:
OK, RESOLVED - JSON Mapping:
{ "summary": "$.event.name", "severity": "$.event.severity", "description": "Trigger: {$.trigger.description}\nHost: {$.host.name}" }
Handle firing and resolved alerts from Grafana dashboards.
- Trigger Field:
$.status - Open Value:
firing - Close Value:
resolved - JSON Mapping:
{ "summary": "$.alerts[0].annotations.summary", "description": "$.alerts[0].annotations.description" }
HookWise provides several ways to automatically map alerts to the correct ConnectWise Client without creating separate endpoints for every customer.
If your monitor name contains #CW followed by a ConnectWise Company Identifier, HookWise will automatically route the ticket to that company.
- Example Monitor Name:
Firewall Down #CW-AcmeCorp - Result: Ticket created for company
AcmeCorp.
Map a specific field in the webhook payload directly to the ConnectWise company ID.
- Mapping:
"customer_id": "$.tags.client_id"
Use Routing Rules to map specific hostnames or message patterns to different companies.
- Rule:
{"path": "$.host", "regex": "PRD-CL1-.*", "overrides": {"customer_id": "CLIENT_A"}}
HookWise provides a centralized mapping table called TenantMap (found in the navbar). This allows you to map common client identifiers (like domains or company IDs) once and apply them globally across all your endpoints.
- Centralized Link: Map
example.com->EXAMPLEjust once. - Auto-Scanning: HookWise intelligently scans incoming payloads for fields like
Tenant,tenantId, and$.TaskInfo.Tenant. - Per-Endpoint Toggle: You can enable or disable TenantMap lookups for each specific endpoint in its configuration form.
Tip
Use TenantMap for high-volume client identification to avoid repeating the same mapping rules in dozens of different endpoint configurations.
After HookWise resolves the ticket's final company, it can associate the ticket with a matching ConnectWise configuration (asset). Enable Automatically link matching ConnectWise configuration on an endpoint to opt in; the feature is disabled by default, including for older backups and cloned endpoints.
HookWise looks for exact identifiers in explicit mappings, common payload fields, the generated ticket title, and the rendered description:
- ConnectWise configuration ID or device ID
- Serial number, MAC address, or tag/asset number
- IP address, including addresses found inside URLs or written with a port/protocol suffix
- Configuration, host, or device name
For example, both 10.70.10.20:7090/tcp and http://10.70.10.20:7090/products/... produce the IP candidate 10.70.10.20. If exactly one active configuration belonging to the ticket's assigned company has that IP, HookWise attaches it to the new or existing ticket. The port is intentionally not part of the configuration match.
Use these optional JSON mapping destinations when a payload has known authoritative fields:
| Destination | Meaning |
|---|---|
configuration_id |
ConnectWise configuration ID |
configuration_device_id |
Device identifier |
configuration_serial |
Serial number |
configuration_mac |
MAC address |
configuration_tag |
Tag or asset number |
configuration_ip |
IP address, URL, or address with an optional port/protocol |
configuration_name |
Configuration, host, or device name |
Matching is deliberately conservative: only active configurations from the exact assigned company are eligible, and ambiguous, conflicting, malformed, inactive, or cross-company results are skipped. Attachment is idempotent and best-effort, so lookup or attachment errors are recorded in webhook history without failing ticket processing.
Warning
A ConnectWise configuration association can affect agreement or SLA selection. Test the endpoint with representative payloads before enabling this feature in production.
Q: Why are tickets not closing automatically?
- Verify that your
Close Valuein the endpoint config matches the payload exactly (e.g.,1vsUP). - Check if the ticket summary has been manually changed in ConnectWise.
Q: "Redis connection refused" in logs?
- Ensure the
rediscontainer is running and theREDIS_PASSWORDmatches in both theredisandhookwiseservices.
Q: AI RCA is too slow?
- LLM inference is CPU-heavy. Ensure the
hookwise-llmcontainer has at least 4 cores and 8GB RAM assigned. - Consider switching to a smaller model (e.g.,
llama3.2:3binstead of larger variants).
Q: Getting "400 Bad Request" when creating tickets?
- This usually means ConnectWise rejected the payload due to a missing or invalid field.
- Check History: The History page shows the exact redacted error returned by ConnectWise in the Error Message column.
- Common causes: Invalid
board,priority, orstatusname that doesn't exist on the target board.
Q: Why was no ConnectWise configuration attached?
- Confirm that auto-linking is enabled for the endpoint and that the ticket resolved to the expected company.
- HookWise requires one exact, active match in that company. Multiple configurations sharing an IP/name, conflicting identifiers, inactive assets, or cross-company results are intentionally skipped.
- Open the webhook's History diagnostics and inspect its configuration-link status for
no_identifiers,no_match,ambiguous,conflict, or an API error.
Q: Metrics at /metrics are missing some counters?
- If you don't see
hookwise_webhooks_totalor other custom metrics, ensure your Celery worker and Web proxy can both reach the same Redis instance. - HookWise uses Redis to aggregate metrics across process boundaries; if Redis is down or partitioned, counters will restart at zero or appear empty.
Q: HMAC verification fails on every request?
- Ensure your monitoring tool is sending the payload as raw JSON.
- If your tool adds extra whitespace or re-orders JSON keys after signing, the signature won't match.
- Ensure all three HMAC headers are present, the timestamp is within five minutes, and every request uses a fresh nonce.
HookWise ships a role-based access model and optional Microsoft Entra ID
single sign-on. Users, roles, permissions, provisioning, and Entra bindings are
managed on the Identity page (/settings/identity, requires user:read).
Each local user manages their own authenticator-app TOTP under Settings.
Permissions follow the resource:action scheme (17 keys, defined in
hookwise/rbac/catalog.py β the code is the single source of truth). Three
built-in roles are seeded and kept up to date automatically:
| Role | Grants |
|---|---|
admin |
All 17 permissions, including user management and system settings. |
operator |
Day-to-day operations including delivery credentials (secret:reveal, secret:rotate), endpoint write/archive/test, replay, tenant mapping, audit β no user management, no settings writes, no history deletion. |
viewer |
Read-only: dashboard, endpoints, history, tenant map, settings view. |
Custom roles can be created in the permission matrix. Each user holds exactly
one role (assigned via the Identity page). Accounts still carrying only the
legacy role column behave as before: admin β admin, user/operator β
operator, viewer β viewer. Revoking a role takes effect on the next request
(permissions epoch), not the next login. The UI hides actions the session
lacks; secrets stay visible but locked, so their existence remains auditable.
| Mode | Behaviour |
|---|---|
on (default) |
Denied requests get a 403 (JSON for APIs, a 403 page for views). |
log |
Checks and logs RBAC(log) warnings, but never blocks β rollout/diagnosis stage. |
off |
No permission checks (pre-RBAC behaviour). |
The routes that always enforced their own boundary (secret reveal/rotate, replay, history operations) keep blocking in every mode.
No database migration is required: an idempotent schema bridge creates the RBAC tables and user columns at startup (Postgres advisory lock, safe with multiple containers booting in parallel).
Local accounts can enroll or disable time-based one-time password (TOTP) authentication under Settings > Two-Factor Auth by scanning a QR code with a standard authenticator app. TOTP secrets are encrypted at rest. An administrator can reset another user's enrollment on the Identity page; that user signs in with only their password until they enroll again. Entra accounts delegate MFA to Microsoft and do not use HookWise TOTP.
Configured entirely through the environment; without it the Entra routes stay inert and local login is unchanged.
| Variable | Usage |
|---|---|
ENTRA_ENABLED |
true activates the sign-in button and callback routes. |
ENTRA_TENANT_ID / ENTRA_CLIENT_ID |
App registration (single tenant; the tenant is verified on every login). |
ENTRA_REDIRECT_URL |
Must match the registration, e.g. https://host/auth/entra/callback. |
ENTRA_CLIENT_SECRET_FILE |
Path to a mounted secret file β the secret never lives in env or DB. |
ENTRA_SCOPES |
Default openid profile email. |
ENTRA_AUTO_PROVISION / ENTRA_AUTO_PROVISION_ROLE |
Start values only; the runtime switch on the Identity page (stored in Redis) takes precedence. |
An optional group filter (Identity page) restricts sign-in to members of one
Entra group. It is enforced fail-closed: with a filter set, a token that carries
no matching groups claim is refused, so the app registration must be
configured to emit group claims (optional claims β groups).
Two provisioning modes, switchable at runtime on the Identity page:
pre-provisioned only (an account must exist here; it binds to the Entra
object on first sign-in) or automatic (any user the tenant assigns gets an
account with the chosen start role β roles holding privileged permissions such
as secret:* or user:manage are rejected as start roles, so auto-provisioning
can never create an administrator). Only the stable tid/oid pair is stored,
never tokens. Entra accounts have no local password or app MFA β both are
Microsoft's job β and their UPN is frozen while bound (clear the binding on the
Identity page to edit it).
Each row on the Identity page expands into a management panel: rename,
set a new password (local accounts, min. 8 characters), reset MFA (shown only
when enrolled), clear the Entra binding, enable/disable and delete. Two
invariants are enforced server-side: the last active holder of user:manage
cannot be deactivated or deleted, and you cannot delete your own account.
Note for headless/API clients: HTTP Basic Auth authenticates against
GUI_USERNAME/GUI_PASSWORD and requires an active HookWise account with
that username. Treat it as a trusted automation credential and restrict its
source addresses with GUI_TRUSTED_IPS.
- Data handling: Webhook payloads are retained in history for troubleshooting. Values under recognized sensitive keys such as
tokenandpasswordare masked in history/API representations and live activity events; database access must still be treated as sensitive. - Encryption: Bearer tokens and HMAC secrets are encrypted using AES-128 via the Fernet protocol.
- Secure Identifiers: Uses high-entropy, 64-character URL-safe tokens for endpoint IDs to prevent brute-force discovery.
- Air-Gap Support: All assets (Bootstrap, Socket.io, Prism.js) are bundled locally. No external CDNs are used.
- Content-versioned assets: Every local static file receives a 12-character SHA-256 content version at application startup. Current versioned URLs are cached as immutable for one year, while stale or unversioned assets revalidate; dynamic and protected responses use no-store/no-cache headers.
We use ruff for code quality:
ruff check .
ruff format .Run the full local quality suite before opening a pull request:
ruff check .
ruff format --check .
mypy .
pytest tests/ -v
python -m pip_audit -r requirements.txt
flask db checkCI runs these checks with Python 3.14.7 against PostgreSQL and Redis. Dependencies are installed from the hash-pinned requirements-dev.txt file.
When changing models.py:
flask db migrate -m "Description"
flask db upgradeMIT License - Copyright (c) 2026 HookWise Team.
