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 tests
- tests/pipedrive/- Pipedrive integration tests
- tests/tc2/- TC2 integration tests
- tests/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 Pipedrive
- Company- Organizations (customers/prospects)
- Contact- Individual contacts within companies
- Deal- Sales opportunities
- Meeting- Scheduled calls/meetings
- Pipeline- Sales pipelines from Pipedrive
- Stage- Pipeline stages
- Config- 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
