Sales integration system connecting TutorCruncher (TC2), Pipedrive CRM, and the Website Callbooker.
Hermes synchronizes customer data between three systems:
- TutorCruncher (TC2) - Internal business management system
- Pipedrive - CRM for sales pipeline management
- Website Callbooker - Customer-facing meeting booking system
Data Flow:
- TC2 → Hermes → Pipedrive
- Callbooker → Hermes → Pipedrive
- Pipedrive → Hermes (updates Hermes only, doesn't sync back to TC2)
Objects are named differently depending on the system:
| Hermes | TutorCruncher | Pipedrive | Description |
|---|---|---|---|
| Company | Cligency | Organisation | A business that is a potential/current customer of TutorCruncher |
| Contact | SR | Person | Someone who works for the Company |
| Deal | Deal | A potential sale with a Company | |
| Meeting | Activity | A meeting with a Contact | |
| Pipeline | Pipeline | The sales pipelines in Pipedrive | |
| Stage | Stage | The stages within each pipeline |
- Language: Python 3.12+
- Framework: FastAPI
- Database: PostgreSQL
- ORM: SQLModel (async SQLAlchemy + Pydantic)
- Validation: Pydantic
- Migrations: Alembic
- Observability: Logfire
- Error Tracking: Sentry
- External APIs: TC2, Pipedrive, Google Calendar
hermes/
├── app/
│ ├── main_app/ # Core Hermes models and views
│ │ ├── models.py # Database models (Company, Contact, Deal, etc.)
│ │ └── views.py # Core API endpoints
│ ├── pipedrive/ # Pipedrive CRM integration
│ │ ├── models.py # Pydantic models for webhooks
│ │ ├── field_mappings.py # Field ID mappings
│ │ ├── api.py # Pipedrive API client
│ │ ├── tasks.py # Background sync tasks
│ │ ├── process.py # Webhook processing logic
│ │ └── views.py # Webhook endpoints
│ ├── tc2/ # TutorCruncher integration
│ │ ├── models.py # TC2 webhook schemas
│ │ ├── api.py # TC2 API client
│ │ ├── process.py # TC2 data processing
│ │ └── views.py # TC2 webhook endpoints
│ ├── callbooker/ # Website callbooker integration
│ │ ├── models.py # Booking request schemas
│ │ ├── views.py # Booking endpoints
│ │ ├── process.py # Booking logic
│ │ ├── availability.py # Slot calculation
│ │ └── google.py # Google Calendar integration
│ ├── core/ # Core infrastructure
│ │ ├── config.py # Settings and configuration
│ │ ├── database.py # Database setup
│ │ └── logging.py # Logging configuration
│ └── main.py # FastAPI application entry point
├── tests/ # Test suite
├── migrations/ # Alembic database migrations
├── system_setup.py # Automated setup script
└── pyproject.toml # Dependencies and configuration
- Python 3.12+
- PostgreSQL
- Access to:
- TutorCruncher (TC2) instance
- Pipedrive account
- Google Cloud project (for calendar integration)
- Clone and install dependencies:
git clone https://github.com/tutorcruncher/hermes.git
cd hermes
make install-dev- Create database:
make reset-db- Configure environment:
Create a
.envfile in the project root:
PD_API_KEY=your_pipedrive_api_key
TC2_API_KEY=your_tc2_api_key
G_PRIVATE_KEY_ID=your-key-id
G_PRIVATE_KEY=your-private-key
LOGFIRE_TOKEN=your-logfire-token- In TC2, navigate to Settings > API Integrations
- Create a new integration named "Hermes"
- Set URL to
http://localhost:8000/tc2/callback/(or your ngrok URL) - Copy the generated API key and set it as
TC2_API_KEYin your.env
Create admin users for different roles:
- PAYG/Startup sales person
- Enterprise sales person
- BDR (Business Development Representative)
- Support staff (1-2 people)
Note their TC2 admin IDs - you'll need them when creating Hermes Admin records.
- In Pipedrive, go to Settings > Personal Preferences > API
- Copy your API key and set it as
PD_API_KEYin your.env
- Navigate to Settings > Tools and Apps > Webhooks
- Create a new webhook:
- Event action:
*(all) - Event object:
*(all) - Endpoint URL:
http://localhost:8000/pipedrive/callback/(or your ngrok URL) - HTTP Auth: None
- Event action:
Create users for each role:
- PAYG/Startup sales
- Enterprise sales
- BDR
- Support (optional)
To get each user's Pipedrive Owner ID:
- Go to Settings > Manage users
- Click on a user
- Copy the number from the end of the URL (e.g.,
123456789)
make setupThis interactive command will:
- Fetch all pipelines and stages from your Pipedrive account
- Let you select the default entry stage for each pipeline
- Configure which pipelines to use for PAYG, Startup, and Enterprise clients
- Store the configuration in the database
make setup-fieldsThis command will:
- Fetch all custom fields from your Pipedrive account
- Show which fields exist and which need to be created
- Generate a
field_mappings_override.pyfile with your field IDs
If make setup-fields shows missing fields, create them in Pipedrive:
Organization Fields:
| Field Name | Type | Description |
|---|---|---|
| hermes_id | Numerical | Internal Hermes ID |
| tc2_status | Large text | TC2 status |
| tc2_cligency_url | Large text | Link to TC2 client |
| paid_invoice_count | Numerical | Number of paid invoices |
| website | Large text | Company website |
| price_plan | Large text | Plan: payg/startup/enterprise |
| estimated_income | Large text | Estimated monthly income |
| support_person_id | Numerical | Support person PD ID |
| bdr_person_id | Numerical | BDR person PD ID |
| signup_questionnaire | Large text | Signup responses |
| utm_source | Large text | UTM source |
| utm_campaign | Large text | UTM campaign |
| created | Date | Date created |
| pay0_dt | Date | First payment date |
| pay1_dt | Date | Second payment date |
| pay3_dt | Date | Third payment date |
| gclid | Large text | Google Click ID |
| gclid_expiry_dt | Date | GCLID expiry date |
| email_confirmed_dt | Date | Email confirmation date |
| card_saved_dt | Date | Card saved date |
Person Fields:
| Field Name | Type | Description |
|---|---|---|
| hermes_id | Numerical | Internal Hermes ID |
Deal Fields:
| Field Name | Type | Description |
|---|---|---|
| hermes_id | Numerical | Internal Hermes ID |
| All Company fields | Same as above | Deal inherits company fields |
After creating fields, run make setup-fields again to update your field mappings.
Use the automated setup command to create admin records from your Pipedrive users:
make setup-adminsThis command will:
- Fetch all users from your Pipedrive account
- Show existing admin records
- Display available Pipedrive users to create admins for
- Let you select users (by index or "all")
- For each selected user, ask for their TC2 Admin ID
- Automatically configure them as:
- Sales and support persons
- Selling all plans (PAYG, Startup, Enterprise)
- Selling to all regions (GB, US, AU, CA, EU, ROW)
Example session:
Existing Admins:
┌────┬────────────────┬──────────────────────┬────────┬──────────┐
│ ID │ Name │ Email │ TC2 ID │ PD ID │
└────┴────────────────┴──────────────────────┴────────┴──────────┘
Fetching users from Pipedrive...
Pipedrive Users:
┌───────┬──────────────┬───────────────────┬──────────┬────────┐
│ Index │ Name │ Email │ PD ID │ Active │
├───────┼──────────────┼───────────────────┼──────────┼────────┤
│ 1 │ John Smith │ [email protected] │ 12345678 │ ✓ │
│ 2 │ Jane Doe │ [email protected] │ 87654321 │ ✓ │
└───────┴──────────────┴───────────────────┴──────────┴────────┘
Select users to create admin records for:
Enter indices separated by commas (e.g., 1,3,5) or "all" for all users
Selection: 1,2
Creating admin for: John Smith
TC2 Admin ID (leave empty to skip): 1
✓ Created admin: John Smith (ID: 1)
Creating admin for: Jane Doe
TC2 Admin ID (leave empty to skip): 2
✓ Created admin: Jane Doe (ID: 2)
✓ Admin setup complete!
Start the development server:
make runThe server will start on http://localhost:8000 with auto-reload enabled.
For production, use:
uvicorn app.main:app --host 0.0.0.0 --port 8000Since TC2 and Pipedrive need to send webhooks to Hermes, expose your local server:
ngrok http 8000Then update your webhook URLs in TC2 and Pipedrive to use the ngrok URL.
When you need to add a new custom field:
-
Create in Pipedrive:
- Navigate to Settings > Data Fields
- Create field with snake_case name (e.g.,
new_field) - Copy the field ID (API key)
-
Add to Hermes models:
- Add to
app/main_app/models.pyin relevant model (Company, Contact, Deal):new_field: Optional[str] = Field(default=None)
- Add to
-
Update field mappings:
- Edit
field_mappings_override.py:COMPANY_PD_FIELD_MAP = { # ... existing fields ... 'new_field': 'your-field-id-from-pipedrive', }
- Edit
-
Update Pydantic models:
- Add to
app/pipedrive/models.py:new_field: Optional[str] = Field( default=None, validation_alias=COMPANY_PD_FIELD_MAP['new_field'] )
- Add to
-
Create migration:
make migrate-create msg="Add new_field" make migrate -
Restart the application
Run the test suite:
make testRun with coverage:
make test-covTests are organized by module:
tests/main_app/- Core functionality teststests/pipedrive/- Pipedrive integration teststests/tc2/- TC2 integration teststests/callbooker/- Callbooker tests
Coverage Target: 95%+
Run make setup-fields to check which fields are missing in Pipedrive. Create any missing fields and update field_mappings_override.py.
- Check ngrok is running and URL is correct
- Verify webhook configuration in Pipedrive/TC2
- Check Hermes logs for errors
- Test webhook with curl:
curl -X POST http://localhost:8000/pipedrive/callback/ \ -H "Content-Type: application/json" \ -d '{"event":"added","current":{}}'
Ensure Admin records exist with correct TC2/Pipedrive IDs. Check pd_owner_id matches actual Pipedrive user IDs.
- Run
make setupto sync pipelines from Pipedrive - Ensure at least one pipeline exists in Pipedrive
- Check Config record has valid pipeline assignments
If migrations fail:
make reset-db # Drops and recreates database
make setup # Reconfigure pipelinesCore Models:
Admin- Sales/support personnel linked to TC2 and PipedriveCompany- Organizations (customers/prospects)Contact- Individual contacts within companiesDeal- Sales opportunitiesMeeting- Scheduled calls/meetingsPipeline- Sales pipelines from PipedriveStage- Pipeline stagesConfig- Application configuration
Key Features:
- All dates are timezone-aware (UTC)
- Foreign key relationships between models
- Unique constraints on external IDs
- Field mappings for Pipedrive custom fields
Hermes uses a centralized field mapping system instead of database tables for custom fields:
- Default mappings are defined in
app/pipedrive/field_mappings.py - Local overrides can be added in
field_mappings_override.py(gitignored) - Pydantic models use
validation_aliasto map incoming webhook data - Sync tasks use the mappings to send data to Pipedrive
This approach:
- Single source of truth for field IDs
- Type-safe at compile time
- Easy to update for new Pipedrive accounts
- No database queries for field lookups
TC2 → Hermes → Pipedrive:
- TC2 sends webhook when client/SR changes
- Hermes processes webhook and updates database
- Background task syncs changes to Pipedrive
Callbooker → Hermes → Pipedrive:
- Website sends booking request
- Hermes creates/updates Company, Contact, Deal
- Meeting is created in Google Calendar
- Changes sync to Pipedrive
Pipedrive → Hermes:
- Pipedrive sends webhook on changes
- Hermes updates local database
- Does NOT sync back to TC2 (one-way)
- Use single quotes for strings
- Modern type hints:
str | Noneinstead ofOptional[str] - Line length: 120 characters
- Format with ruff:
make format - Lint with ruff:
make lint
- Write tests for all new features
- Maintain 95%+ code coverage
- Use test fixtures from
tests/conftest.py - Follow existing test patterns (see
AGENTS.md)
- Use clear, descriptive commit messages
- Reference issue numbers where applicable
- Keep commits focused and atomic
Proprietary - TutorCruncher Ltd
