Growably Integration

Five-phase plan to wire Growably (LeadConnector) into Leif as a CRM and proactive engagement surface.

Mirrored from superhitech/leif:docs/plans/growably-integration-plan.md. The repo copy is the source of truth — this page is a browsable mirror.

Growably / LeadConnector Integration — Leif MCP Implementation Plan

Context

Growably is a white-labeled version of LeadConnector (HighLevel/GoHighLevel platform). SuperHiTech has a Growably subscription through Tech Tribe. The platform has been mostly unused but has full API capability via the LeadConnector V2 REST API. Wayne has created a Private Integration token with all scopes enabled (read + write on every resource).

The goal is to give Leif operational access to Growably so it can drive proactive customer engagement, pipeline tracking, and eventually marketing automation — without Wayne needing to log into another portal.

API Reference

  • Base URL: https://services.leadconnectorhq.com
  • Auth: Authorization: Bearer <PRIVATE_INTEGRATION_TOKEN> (static, no refresh needed)
  • Required Header: Version: 2021-07-28 (required on ALL v2 requests)
  • Content-Type: application/json
  • Rate Limits: 100 requests / 10 seconds, 200,000 / day per location
  • Pagination: Cursor-based using startAfter and startAfterId from response meta object; limit param (max 100)
  • Location ID: Required as query param (locationId) on most list/create endpoints. Retrieve via GET /locations/search or GET /locations/{locationId} — store in env var GROWABLY_LOCATION_ID

Key Endpoint Reference

Contacts:

  • GET /contacts/ — list contacts (params: locationId, limit, query, startAfter, startAfterId)
  • GET /contacts/{contactId} — get single contact
  • POST /contacts/ — create contact (body: firstName, lastName, email, phone, locationId, tags[], source, customFields[])
  • PUT /contacts/{contactId} — update contact
  • DELETE /contacts/{contactId} — delete contact
  • POST /contacts/upsert — create or update by email/phone match
  • GET /contacts/{contactId}/tasks — contact tasks
  • GET /contacts/{contactId}/notes — contact notes
  • POST /contacts/{contactId}/notes — add note
  • POST /contacts/{contactId}/tags — add tags
  • DELETE /contacts/{contactId}/tags — remove tags
  • POST /contacts/{contactId}/workflow/{workflowId} — trigger workflow on contact

Conversations:

  • GET /conversations/ — list conversations (params: locationId, contactId)
  • GET /conversations/{conversationId} — get conversation
  • GET /conversations/{conversationId}/messages — get messages
  • POST /conversations/messages — send message (SMS, email, etc.)

Calendars:

  • GET /calendars/ — list calendars (params: locationId)
  • GET /calendars/events — list calendar events
  • POST /calendars/events/appointments — create appointment

Opportunities (Pipeline):

  • GET /opportunities/ — list opportunities (params: locationId, pipelineId, stageId)
  • GET /opportunities/{opportunityId} — get opportunity
  • POST /opportunities/ — create opportunity
  • PUT /opportunities/{opportunityId} — update opportunity
  • DELETE /opportunities/{opportunityId} — delete opportunity
  • GET /opportunities/pipelines — list pipelines

Workflows:

  • GET /workflows/ — list workflows (params: locationId)

Custom Fields:

  • GET /locations/{locationId}/customFields — list custom fields
  • POST /locations/{locationId}/customFields — create custom field

Tags:

  • GET /locations/{locationId}/tags — list all tags

Location:

  • GET /locations/{locationId} — get location details
  • GET /locations/search — search locations

Invoices:

  • GET /invoices/ — list invoices
  • POST /invoices/ — create invoice

Forms & Surveys:

  • GET /forms/ — list forms
  • GET /forms/submissions — list form submissions
  • GET /surveys/ — list surveys

Architecture

Follow the existing Leif patterns exactly:

src/integrations/growably_client.py    — API client (extends BaseAPIClient)
src/mcp/tools/growably.py              — MCP tool definitions + register()
src/mcp/tools/__init__.py              — Add import + register call
.env                                   — Add GROWABLY_API_TOKEN, GROWABLY_LOCATION_ID

Environment Variables

Add to .env:

GROWABLY_API_TOKEN=<private integration token>
GROWABLY_LOCATION_ID=<location id - discover via API after token is set>

Implementation Phases

Phase 1: Client + Read-Only Discovery Tools

Goal: Get connected, see what data exists, validate API access.

Task 1.1: Create src/integrations/growably_client.py

Extend BaseAPIClient following the same pattern as hudu_client.py:

class GrowablyAPIError(APIError):
    """Growably/LeadConnector-specific API error."""

class GrowablyClient(BaseAPIClient):
    _error_class = GrowablyAPIError
    _service_name = "Growably"

    def __init__(self, api_token=None, location_id=None, timeout=30):
        self.api_token = api_token or os.getenv("GROWABLY_API_TOKEN", "")
        self.location_id = location_id or os.getenv("GROWABLY_LOCATION_ID", "")
        super().__init__(timeout=timeout)

    def _base_url(self) -> str:
        return "https://services.leadconnectorhq.com"

    def _headers(self) -> Dict[str, str]:
        return {
            "Authorization": f"Bearer {self.api_token}",
            "Version": "2021-07-28",
            "Accept": "application/json",
            "Content-Type": "application/json",
        }

Client methods to implement in Phase 1:

  • get_location()GET /locations/{locationId} — verify connectivity + get location details
  • get_contacts(limit, query, start_after, start_after_id)GET /contacts/ with locationId
  • get_contact(contact_id)GET /contacts/{contactId}
  • search_contacts(query)GET /contacts/ with query param
  • get_pipelines()GET /opportunities/pipelines with locationId
  • get_opportunities(pipeline_id, stage_id, limit)GET /opportunities/
  • get_opportunity(opportunity_id)GET /opportunities/{opportunityId}
  • get_calendars()GET /calendars/ with locationId
  • get_calendar_events(calendar_id, start_time, end_time)GET /calendars/events
  • get_conversations(contact_id)GET /conversations/
  • get_conversation_messages(conversation_id)GET /conversations/{conversationId}/messages
  • get_workflows()GET /workflows/
  • get_tags()GET /locations/{locationId}/tags
  • get_custom_fields()GET /locations/{locationId}/customFields

Pagination note: LeadConnector V2 uses cursor-based pagination. The response includes a meta object with startAfter, startAfterId, and total. Implement a _paginate_cursor() helper (or override paginate() from BaseAPIClient) that passes startAfter and startAfterId as query params on subsequent requests.

Task 1.2: Create src/mcp/tools/growably.py

Follow the consolidated pattern from hudu.py — a register(mcp, growably_service=None) function that defines MCP tools.

Phase 1 MCP tools:

@mcp.tool(name="growably_get")
async def growably_get(
    entity_type: str,      # "contact", "opportunity", "pipeline", "calendar",
                           # "conversation", "workflow", "tag", "custom_field", "location"
    id: Optional[str] = None,
    query: Optional[str] = None,
    pipeline_id: Optional[str] = None,
    contact_id: Optional[str] = None,
    limit: int = 20,
    start_after: Optional[str] = None,
    start_after_id: Optional[str] = None,
) -> str:
    """Get or list Growably/LeadConnector records.

    entity_type: contact, opportunity, pipeline, calendar, conversation,
                 workflow, tag, custom_field, location
    id: Fetch single record by ID (for contact, opportunity, conversation)
    query: Search string (contacts only)
    pipeline_id: Filter opportunities by pipeline
    contact_id: Filter conversations by contact
    limit: Max results (default 20, max 100)
    start_after/start_after_id: Cursor pagination from previous response meta
    """

This single consolidated tool keeps the tool count manageable while covering all read operations.

Task 1.3: Wire into __init__.py

Add to src/mcp/tools/__init__.py:

  • Import: from .growably import register as register_growably
  • Add tool names to REGISTERED_TOOL_NAMES
  • Add growably_service=None to register_all() signature
  • Call register_growably(mcp, growably_service=growably_service)

Task 1.4: Wire into src/mcp/server.py

  • Import GrowablyClient from integrations
  • Instantiate in server setup
  • Pass to register_all() as growably_service

Task 1.5: Add env vars

  • Add GROWABLY_API_TOKEN and GROWABLY_LOCATION_ID to .env
  • Wayne will provide the actual token value
  • GROWABLY_LOCATION_ID can be discovered by calling GET /locations/search after the token is set — but should be stored as env var once known

Task 1.6: Restart and validate

  • Restart Leif service
  • Test growably_get with entity_type="location" to verify connectivity
  • Test growably_get with entity_type="contact" to see existing data
  • Test growably_get with entity_type="pipeline" to see pipeline structure

Phase 2: Write Operations — Contacts & Opportunities

Goal: Enable creating and updating contacts and opportunities so Leif can manage the CRM.

Task 2.1: Add client write methods

Add to GrowablyClient:

  • create_contact(first_name, last_name, email, phone, tags, source, custom_fields)POST /contacts/
  • update_contact(contact_id, data)PUT /contacts/{contactId}
  • upsert_contact(first_name, last_name, email, phone, tags, source, custom_fields)POST /contacts/upsert
  • delete_contact(contact_id)DELETE /contacts/{contactId}
  • add_contact_tags(contact_id, tags)POST /contacts/{contactId}/tags
  • remove_contact_tags(contact_id, tags)DELETE /contacts/{contactId}/tags
  • add_contact_note(contact_id, body)POST /contacts/{contactId}/notes
  • create_opportunity(pipeline_id, stage_id, contact_id, name, monetary_value, status)POST /opportunities/
  • update_opportunity(opportunity_id, data)PUT /opportunities/{opportunityId}
  • delete_opportunity(opportunity_id)DELETE /opportunities/{opportunityId}

IMPORTANT TAG BEHAVIOR: The V2 API’s PUT contacts endpoint overwrites existing tags. When updating tags, always GET the contact first, merge the tag arrays, then PUT. The add_contact_tags and remove_contact_tags dedicated endpoints avoid this issue.

Task 2.2: Add MCP write tools

@mcp.tool(name="growably_upsert_contact")
async def growably_upsert_contact(
    first_name: str,
    last_name: Optional[str] = None,
    email: Optional[str] = None,
    phone: Optional[str] = None,
    tags: Optional[list[str]] = None,
    source: Optional[str] = None,
    custom_fields: Optional[dict[str, Any]] = None,
) -> str:
    """Create or update a Growably contact (matched by email/phone)."""

@mcp.tool(name="growably_update")
async def growably_update(
    entity_type: str,     # "contact", "opportunity"
    id: str,
    data: dict[str, Any],
) -> str:
    """Update a Growably record."""

@mcp.tool(name="growably_create_opportunity")
async def growably_create_opportunity(
    name: str,
    pipeline_id: str,
    stage_id: str,
    contact_id: str,
    monetary_value: Optional[float] = None,
    status: str = "open",
) -> str:
    """Create a new opportunity in a Growably pipeline."""

@mcp.tool(name="growably_add_note")
async def growably_add_note(
    contact_id: str,
    body: str,
) -> str:
    """Add a note to a Growably contact."""

@mcp.tool(name="growably_manage_tags")
async def growably_manage_tags(
    contact_id: str,
    action: str,           # "add" or "remove"
    tags: list[str],
) -> str:
    """Add or remove tags on a Growably contact."""

Phase 3: Conversations & Workflows

Goal: Enable Leif to send messages and trigger workflows.

Task 3.1: Add client methods

  • send_message(contact_id, message, type)POST /conversations/messages (type: SMS, Email, etc.)
  • trigger_workflow(contact_id, workflow_id)POST /contacts/{contactId}/workflow/{workflowId}

Task 3.2: Add MCP tools

@mcp.tool(name="growably_send_message")
async def growably_send_message(
    contact_id: str,
    message: str,
    type: str = "SMS",        # SMS, Email, etc.
    subject: Optional[str] = None,  # Required for email
    email_from: Optional[str] = None,
) -> str:
    """Send a message to a Growably contact via SMS or Email."""

@mcp.tool(name="growably_trigger_workflow")
async def growably_trigger_workflow(
    contact_id: str,
    workflow_id: str,
) -> str:
    """Trigger a workflow on a Growably contact."""

Safety note: Message sending should require confirmation context — Leif should always tell Wayne what it’s about to send and to whom before executing. Build this into tool description guidance, not code-level blocks.


Phase 4: RepairShopr → Growably Contact Sync

Goal: One-time and ongoing sync of active RepairShopr customers into Growably.

Task 4.1: Build sync script

Create src/jobs/growably_contact_sync.py:

  1. Fetch all active customers from RepairShopr (repairshopr_client.get_customers())
  2. For each customer, upsert into Growably via growably_client.upsert_contact():
    • Map: RS firstnamefirstName, lastnamelastName, emailemail, phonephone
    • Tag with: repairshopr, service tier if known, location (Sheldon/Orange City/Sioux Center)
    • Store RS customer ID in a Growably custom field (repairshopr_id) for cross-reference
    • Source: "repairshopr-sync"
  3. Log results: created count, updated count, errors
  4. Handle rate limiting (100 req/10s — add small delay between batches)

Task 4.2: Create custom field for cross-reference

Before running sync:

  • Create custom field repairshopr_id in Growably via POST /locations/{locationId}/customFields
  • Type: TEXT, field name: RepairShopr ID

Task 4.3: Optional — schedule as cron job

If ongoing sync is desired, register as a Leif scheduled job that runs daily or weekly.


Phase 5: Proactive Engagement Automation

Goal: Leif-driven proactive customer communication.

This phase is more workflow/strategy than code. Once Phases 1-4 are complete, the tools exist for Leif to:

  1. Agreement renewal reminders — Query RepairShopr for upcoming agreement expirations, find matching Growably contact, trigger reminder workflow or add to pipeline
  2. Dormant client check-ins — Find RS customers with no tickets in 90+ days, trigger a check-in email workflow in Growably
  3. Quarterly business reviews — Tag top clients, trigger QBR scheduling workflow
  4. Security alert broadcasts — When a major vulnerability drops, send SMS/email to all managed services clients via Growably

These would be implemented as Leif scheduled jobs or triggered manually through conversation.


File Inventory (What Gets Created/Modified)

New Files

FileDescription
src/integrations/growably_client.pyAPI client extending BaseAPIClient
src/mcp/tools/growably.pyMCP tool definitions with register() function
src/jobs/growably_contact_sync.pyRepairShopr → Growably contact sync job (Phase 4)

Modified Files

FileChange
src/mcp/tools/__init__.pyAdd import, tool names, register call
src/mcp/server.pyInstantiate GrowablyClient, pass to register_all
.envAdd GROWABLY_API_TOKEN, GROWABLY_LOCATION_ID

No changes needed to

  • src/integrations/base_client.py — use as-is
  • src/integrations/__init__.py — only if it has an __all__ or lazy imports

Implementation Notes for Claude Code

  1. Follow existing patterns exactly. Look at hudu_client.py for client structure and src/mcp/tools/hudu.py for MCP tool registration patterns. The consolidated _get / _create / _update pattern with entity_type routing is the preferred approach.

  2. Cursor pagination is different from page-based. BaseAPIClient’s paginate() uses page numbers. LeadConnector V2 uses startAfter/startAfterId cursors. Either add a _paginate_cursor() method to GrowablyClient or handle it in each list method directly. Don’t try to force it into the existing paginate() interface.

  3. The locationId query param is required on most list endpoints. Store it as self.location_id and inject automatically — callers shouldn’t have to pass it every time.

  4. Private Integration tokens don’t expire. No refresh logic needed. The token goes directly in the Authorization header as Bearer <token> (not as Bearer Bearer <token> — the token value itself does NOT include the word “Bearer”).

  5. Tag overwrites on PUT. This is a known V2 API bug/behavior. Use the dedicated tag endpoints (POST/DELETE /contacts/{id}/tags) instead of updating tags via PUT contact.

  6. Error responses from the API typically look like {"statusCode": 4xx, "message": "...", "error": "..."}. The BaseAPIClient _process_response will handle status codes, but consider parsing the message field for better error messages.

  7. Custom fields in contact create/upsert use this format:

    {
      "customFields": [
        {"id": "field_id_here", "value": "field_value"}
      ]
    }

    The field id must be looked up from GET /locations/{locationId}/customFields first.

  8. Test order: Phase 1 tasks should be completed and tested before moving to Phase 2. The read-only tools will reveal the actual state of the Growably account (whether it has existing data, what pipelines exist, etc.) which informs Phase 2+ implementation.

  9. Service enable check: Follow the pattern from hudu.py — check that api_token and location_id are set before allowing tool calls. Return a clear error if the integration isn’t configured.

  10. Tool naming convention: All tools prefixed with growably_ to match the pattern of other integrations (hudu_, repairshopr_, cwa_, etc.).


Definition of Done

  • Phase 1: growably_get tool works for all entity types, Leif can query contacts/pipelines/calendars
  • Phase 2: Leif can create/update/upsert contacts and manage opportunities
  • Phase 3: Leif can send messages and trigger workflows
  • Phase 4: RepairShopr customer base is synced into Growably with proper tagging
  • Phase 5: At least one proactive automation is running (e.g., dormant client check-in)