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
startAfterandstartAfterIdfrom responsemetaobject;limitparam (max 100) - Location ID: Required as query param (
locationId) on most list/create endpoints. Retrieve viaGET /locations/searchorGET /locations/{locationId}— store in env varGROWABLY_LOCATION_ID
Key Endpoint Reference
Contacts:
GET /contacts/— list contacts (params:locationId,limit,query,startAfter,startAfterId)GET /contacts/{contactId}— get single contactPOST /contacts/— create contact (body:firstName,lastName,email,phone,locationId,tags[],source,customFields[])PUT /contacts/{contactId}— update contactDELETE /contacts/{contactId}— delete contactPOST /contacts/upsert— create or update by email/phone matchGET /contacts/{contactId}/tasks— contact tasksGET /contacts/{contactId}/notes— contact notesPOST /contacts/{contactId}/notes— add notePOST /contacts/{contactId}/tags— add tagsDELETE /contacts/{contactId}/tags— remove tagsPOST /contacts/{contactId}/workflow/{workflowId}— trigger workflow on contact
Conversations:
GET /conversations/— list conversations (params:locationId,contactId)GET /conversations/{conversationId}— get conversationGET /conversations/{conversationId}/messages— get messagesPOST /conversations/messages— send message (SMS, email, etc.)
Calendars:
GET /calendars/— list calendars (params:locationId)GET /calendars/events— list calendar eventsPOST /calendars/events/appointments— create appointment
Opportunities (Pipeline):
GET /opportunities/— list opportunities (params:locationId,pipelineId,stageId)GET /opportunities/{opportunityId}— get opportunityPOST /opportunities/— create opportunityPUT /opportunities/{opportunityId}— update opportunityDELETE /opportunities/{opportunityId}— delete opportunityGET /opportunities/pipelines— list pipelines
Workflows:
GET /workflows/— list workflows (params:locationId)
Custom Fields:
GET /locations/{locationId}/customFields— list custom fieldsPOST /locations/{locationId}/customFields— create custom field
Tags:
GET /locations/{locationId}/tags— list all tags
Location:
GET /locations/{locationId}— get location detailsGET /locations/search— search locations
Invoices:
GET /invoices/— list invoicesPOST /invoices/— create invoice
Forms & Surveys:
GET /forms/— list formsGET /forms/submissions— list form submissionsGET /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 detailsget_contacts(limit, query, start_after, start_after_id)—GET /contacts/with locationIdget_contact(contact_id)—GET /contacts/{contactId}search_contacts(query)—GET /contacts/withqueryparamget_pipelines()—GET /opportunities/pipelineswith locationIdget_opportunities(pipeline_id, stage_id, limit)—GET /opportunities/get_opportunity(opportunity_id)—GET /opportunities/{opportunityId}get_calendars()—GET /calendars/with locationIdget_calendar_events(calendar_id, start_time, end_time)—GET /calendars/eventsget_conversations(contact_id)—GET /conversations/get_conversation_messages(conversation_id)—GET /conversations/{conversationId}/messagesget_workflows()—GET /workflows/get_tags()—GET /locations/{locationId}/tagsget_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=Nonetoregister_all()signature - Call
register_growably(mcp, growably_service=growably_service)
Task 1.4: Wire into src/mcp/server.py
- Import
GrowablyClientfrom integrations - Instantiate in server setup
- Pass to
register_all()asgrowably_service
Task 1.5: Add env vars
- Add
GROWABLY_API_TOKENandGROWABLY_LOCATION_IDto.env - Wayne will provide the actual token value
GROWABLY_LOCATION_IDcan be discovered by callingGET /locations/searchafter the token is set — but should be stored as env var once known
Task 1.6: Restart and validate
- Restart Leif service
- Test
growably_getwithentity_type="location"to verify connectivity - Test
growably_getwithentity_type="contact"to see existing data - Test
growably_getwithentity_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/upsertdelete_contact(contact_id)—DELETE /contacts/{contactId}add_contact_tags(contact_id, tags)—POST /contacts/{contactId}/tagsremove_contact_tags(contact_id, tags)—DELETE /contacts/{contactId}/tagsadd_contact_note(contact_id, body)—POST /contacts/{contactId}/notescreate_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:
- Fetch all active customers from RepairShopr (
repairshopr_client.get_customers()) - For each customer, upsert into Growably via
growably_client.upsert_contact():- Map: RS
firstname→firstName,lastname→lastName,email→email,phone→phone - 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"
- Map: RS
- Log results: created count, updated count, errors
- 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_idin Growably viaPOST /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:
- Agreement renewal reminders — Query RepairShopr for upcoming agreement expirations, find matching Growably contact, trigger reminder workflow or add to pipeline
- Dormant client check-ins — Find RS customers with no tickets in 90+ days, trigger a check-in email workflow in Growably
- Quarterly business reviews — Tag top clients, trigger QBR scheduling workflow
- 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
| File | Description |
|---|---|
src/integrations/growably_client.py | API client extending BaseAPIClient |
src/mcp/tools/growably.py | MCP tool definitions with register() function |
src/jobs/growably_contact_sync.py | RepairShopr → Growably contact sync job (Phase 4) |
Modified Files
| File | Change |
|---|---|
src/mcp/tools/__init__.py | Add import, tool names, register call |
src/mcp/server.py | Instantiate GrowablyClient, pass to register_all |
.env | Add GROWABLY_API_TOKEN, GROWABLY_LOCATION_ID |
No changes needed to
src/integrations/base_client.py— use as-issrc/integrations/__init__.py— only if it has an__all__or lazy imports
Implementation Notes for Claude Code
-
Follow existing patterns exactly. Look at
hudu_client.pyfor client structure andsrc/mcp/tools/hudu.pyfor MCP tool registration patterns. The consolidated_get/_create/_updatepattern with entity_type routing is the preferred approach. -
Cursor pagination is different from page-based. BaseAPIClient’s
paginate()uses page numbers. LeadConnector V2 usesstartAfter/startAfterIdcursors. Either add a_paginate_cursor()method toGrowablyClientor handle it in each list method directly. Don’t try to force it into the existingpaginate()interface. -
The
locationIdquery param is required on most list endpoints. Store it asself.location_idand inject automatically — callers shouldn’t have to pass it every time. -
Private Integration tokens don’t expire. No refresh logic needed. The token goes directly in the Authorization header as
Bearer <token>(not asBearer Bearer <token>— the token value itself does NOT include the word “Bearer”). -
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. -
Error responses from the API typically look like
{"statusCode": 4xx, "message": "...", "error": "..."}. The BaseAPIClient_process_responsewill handle status codes, but consider parsing themessagefield for better error messages. -
Custom fields in contact create/upsert use this format:
{ "customFields": [ {"id": "field_id_here", "value": "field_value"} ] }The field
idmust be looked up fromGET /locations/{locationId}/customFieldsfirst. -
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.
-
Service enable check: Follow the pattern from
hudu.py— check thatapi_tokenandlocation_idare set before allowing tool calls. Return a clear error if the integration isn’t configured. -
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_gettool 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)