diff --git a/backend/app/api/endpoints/integrations.py b/backend/app/api/endpoints/integrations.py
index b260fccb..f4a5cd7c 100644
--- a/backend/app/api/endpoints/integrations.py
+++ b/backend/app/api/endpoints/integrations.py
@@ -28,6 +28,7 @@ from app.schemas.psa_connection import (
PsaMemberResponse,
AutoMatchResult,
)
+from app.core.config import settings
from app.services.psa.encryption import (
decrypt_credentials,
encrypt_credentials,
@@ -130,6 +131,9 @@ async def create_connection(
if not current_user.account_id:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "No account associated with user")
+ if not settings.CW_CLIENT_ID:
+ raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, "ConnectWise integration is not configured on this server")
+
# Check for existing connection
existing = await _get_connection(current_user.account_id, db)
if existing:
@@ -145,7 +149,7 @@ async def create_connection(
company_id=data.company_id,
public_key=data.public_key,
private_key=data.private_key,
- client_id=data.client_id,
+ client_id=settings.CW_CLIENT_ID,
)
if not test_result.success:
raise HTTPException(
@@ -156,7 +160,6 @@ async def create_connection(
credentials = {
"public_key": data.public_key,
"private_key": data.private_key,
- "client_id": data.client_id,
}
conn = PsaConnection(
@@ -189,7 +192,7 @@ async def update_connection(
creds = decrypt_credentials(conn.credentials_encrypted)
# Track whether credential fields changed
- cred_fields = {"public_key", "private_key", "client_id"}
+ cred_fields = {"public_key", "private_key"}
cred_changed = False
# Apply updates
@@ -213,7 +216,7 @@ async def update_connection(
company_id=company_id_val,
public_key=creds["public_key"],
private_key=creds["private_key"],
- client_id=creds["client_id"],
+ client_id=settings.CW_CLIENT_ID or "",
)
if not test_result.success:
raise HTTPException(
@@ -263,7 +266,7 @@ async def test_connection(
company_id=conn.company_id,
public_key=creds["public_key"],
private_key=creds["private_key"],
- client_id=creds["client_id"],
+ client_id=settings.CW_CLIENT_ID or "",
)
if result.success:
diff --git a/backend/app/core/config.py b/backend/app/core/config.py
index 0cd64e94..ee0ea9db 100644
--- a/backend/app/core/config.py
+++ b/backend/app/core/config.py
@@ -119,6 +119,16 @@ class Settings(BaseSettings):
"""Check if any AI provider is configured."""
return self.ANTHROPIC_API_KEY is not None or self.GOOGLE_AI_API_KEY is not None
+ # ConnectWise PSA Integration
+ # CW_CLIENT_ID is a product-level GUID registered at developer.connectwise.com
+ # All MSP customers share this single clientId — it identifies ResolutionFlow as the integration
+ CW_CLIENT_ID: Optional[str] = None
+
+ @property
+ def cw_enabled(self) -> bool:
+ """Check if ConnectWise integration is configured."""
+ return self.CW_CLIENT_ID is not None
+
# Monitoring
SENTRY_DSN: Optional[str] = None
diff --git a/backend/app/schemas/psa_connection.py b/backend/app/schemas/psa_connection.py
index 5231a201..d9dfeeb4 100644
--- a/backend/app/schemas/psa_connection.py
+++ b/backend/app/schemas/psa_connection.py
@@ -12,7 +12,7 @@ class PsaConnectionCreate(BaseModel):
company_id: str = Field(min_length=1, max_length=100)
public_key: str = Field(min_length=1)
private_key: str = Field(min_length=1)
- client_id: str = Field(min_length=1)
+ # Note: client_id is NOT per-MSP — it's a product-level GUID from settings.CW_CLIENT_ID
class PsaConnectionUpdate(BaseModel):
@@ -21,7 +21,6 @@ class PsaConnectionUpdate(BaseModel):
company_id: str | None = None
public_key: str | None = None
private_key: str | None = None
- client_id: str | None = None
class PsaConnectionResponse(BaseModel):
diff --git a/backend/app/services/psa/connectwise/provider.py b/backend/app/services/psa/connectwise/provider.py
index 3159e9ba..d84ef73b 100644
--- a/backend/app/services/psa/connectwise/provider.py
+++ b/backend/app/services/psa/connectwise/provider.py
@@ -199,6 +199,10 @@ class ConnectWiseProvider(PSAProvider):
note_flags = flags.get(note_type, flags[NoteType.INTERNAL_ANALYSIS])
+ # NOTE: CW Developer Guide states \n is "Not Supported" in JSON bodies
+ # and may be collapsed to a single space. CW does support markdown in ticket
+ # notes (see PSA-Markdown.md). This needs sandbox testing — if newlines are
+ # lost, consider using double-space line breaks or HTML
tags instead.
body: dict = {
"text": text,
**note_flags,
diff --git a/backend/app/services/psa/registry.py b/backend/app/services/psa/registry.py
index 7f4428ac..ff84c3cc 100644
--- a/backend/app/services/psa/registry.py
+++ b/backend/app/services/psa/registry.py
@@ -8,6 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.models.psa_connection import PsaConnection
from app.services.psa.base import PSAProvider
+from app.core.config import settings
from app.services.psa.encryption import decrypt_credentials
from app.services.psa.exceptions import PSAConnectionError
@@ -40,7 +41,7 @@ async def get_provider_for_account(
company_id=connection.company_id,
public_key=creds["public_key"],
private_key=creds["private_key"],
- client_id=creds["client_id"],
+ client_id=settings.CW_CLIENT_ID or "",
)
return ConnectWiseProvider(client)
diff --git a/frontend/src/pages/account/IntegrationsPage.tsx b/frontend/src/pages/account/IntegrationsPage.tsx
index 15076550..ae9a55ac 100644
--- a/frontend/src/pages/account/IntegrationsPage.tsx
+++ b/frontend/src/pages/account/IntegrationsPage.tsx
@@ -28,7 +28,6 @@ interface ConnectionForm {
company_id: string
public_key: string
private_key: string
- client_id: string
}
const emptyForm: ConnectionForm = {
@@ -37,7 +36,6 @@ const emptyForm: ConnectionForm = {
company_id: '',
public_key: '',
private_key: '',
- client_id: '',
}
type Tab = 'connection' | 'member-mapping' | 'post-history'
@@ -122,7 +120,7 @@ export function IntegrationsPage() {
if (form.company_id && form.company_id !== connection.company_id) update.company_id = form.company_id
if (form.public_key) update.public_key = form.public_key
if (form.private_key) update.private_key = form.private_key
- if (form.client_id) update.client_id = form.client_id
+ // client_id is server-side (settings.CW_CLIENT_ID), not per-account
const updated = await integrationsApi.updateConnection(connection.id, update)
setConnection(updated)
@@ -177,7 +175,6 @@ export function IntegrationsPage() {
company_id: connection.company_id,
public_key: '',
private_key: '',
- client_id: '',
})
setFormError(null)
setTestResult(null)
@@ -336,20 +333,6 @@ export function IntegrationsPage() {
/>
-