-
Notifications
You must be signed in to change notification settings - Fork 76
Add HubSpot contacts to Google Sheets prebuilt integration #128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| { | ||
| "$schema": "http://json-schema.org/draft-07/schema#", | ||
| "type": "object", | ||
| "properties": { | ||
| "wso2": { | ||
| "type": "object", | ||
| "properties": { | ||
| "hubspot_contacts_to_google_sheet": { | ||
| "type": "object", | ||
| "properties": { | ||
| "hubspotAccessToken": { | ||
| "type": "string", | ||
| "description": "Legacy app access token used to authenticate with the HubSpot CRM API." | ||
| }, | ||
| "googleClientId": { | ||
| "type": "string", | ||
| "description": "OAuth 2.0 client ID for authenticating with the Google Sheets API." | ||
| }, | ||
| "googleClientSecret": { | ||
| "type": "string", | ||
| "description": "OAuth 2.0 client secret for authenticating with the Google Sheets API." | ||
| }, | ||
| "googleRefreshToken": { | ||
| "type": "string", | ||
| "description": "OAuth 2.0 refresh token used to obtain new access tokens for the Google Sheets API." | ||
| }, | ||
| "googleRefreshUrl": { | ||
| "type": "string", | ||
| "description": "The token endpoint URL used to refresh Google OAuth 2.0 access tokens." | ||
| }, | ||
| "spreadsheetId": { | ||
| "type": "string", | ||
| "description": "The unique identifier of the Google Spreadsheet where HubSpot contacts will be synced." | ||
| }, | ||
| "subscriberSheetName": { | ||
| "type": "string", | ||
| "description": "Name of the worksheet for contacts with lifecycle stage 'Subscriber'." | ||
| }, | ||
| "leadSheetName": { | ||
| "type": "string", | ||
| "description": "Name of the worksheet for contacts with lifecycle stage 'Lead'." | ||
| }, | ||
| "marketingqualifiedleadSheetName": { | ||
| "type": "string", | ||
| "description": "Name of the worksheet for contacts with lifecycle stage 'Marketing Qualified Lead'." | ||
| }, | ||
| "salesqualifiedleadSheetName": { | ||
| "type": "string", | ||
| "description": "Name of the worksheet for contacts with lifecycle stage 'Sales Qualified Lead'." | ||
| }, | ||
| "opportunitySheetName": { | ||
| "type": "string", | ||
| "description": "Name of the worksheet for contacts with lifecycle stage 'Opportunity'." | ||
| }, | ||
| "customerSheetName": { | ||
| "type": "string", | ||
| "description": "Name of the worksheet for contacts with lifecycle stage 'Customer'." | ||
| }, | ||
| "evangelistSheetName": { | ||
| "type": "string", | ||
| "description": "Name of the worksheet for contacts with lifecycle stage 'Evangelist'." | ||
| }, | ||
| "otherSheetName": { | ||
| "type": "string", | ||
| "description": "Name of the worksheet for contacts with lifecycle stage 'Other'." | ||
| }, | ||
| "defaultSheetName": { | ||
| "type": "string", | ||
| "description": "Name of the fallback worksheet for contacts whose lifecycle stage does not match any known value." | ||
| }, | ||
| "fields": { | ||
| "type": "array", | ||
| "items": { | ||
| "type": "string" | ||
| }, | ||
| "description": "List of HubSpot contact property names to include as columns in the Google Sheet (e.g. ['email', 'firstname', 'lastname', 'phone'])." | ||
| }, | ||
| "lastSyncTimestamp": { | ||
| "type": "string", | ||
| "description": "ISO 8601 timestamp used as the starting checkpoint for incremental sync. Leave empty to perform a full sync on the first run." | ||
| }, | ||
| "contactFilterProperty": { | ||
| "type": "string", | ||
| "description": "HubSpot contact property name to filter synced contacts by (e.g. 'hs_lead_status'). Leave empty to sync all contacts." | ||
| }, | ||
| "contactFilterValue": { | ||
| "type": "string", | ||
| "description": "Expected value of the filter property. Only contacts matching this value will be synced." | ||
| }, | ||
| "maxRows": { | ||
| "type": "integer", | ||
| "description": "Maximum number of contacts to process per incremental sync run. Set to 0 for no limit." | ||
| }, | ||
| "syncMode": { | ||
| "type": "string", | ||
| "description": "Sync strategy: 'upsert' (default) updates existing rows and inserts new ones; 'append' always inserts; 'replace' clears the sheet then inserts all contacts." | ||
| } | ||
| }, | ||
| "additionalProperties": false, | ||
| "required": [ | ||
| "hubspotAccessToken", | ||
| "googleClientId", | ||
| "googleClientSecret", | ||
| "googleRefreshToken", | ||
| "spreadsheetId" | ||
| ] | ||
| } | ||
| }, | ||
| "additionalProperties": false | ||
| } | ||
| }, | ||
| "additionalProperties": false, | ||
| "requiredLevel": 3 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| ```mermaid | ||
| flowchart TD | ||
| A([Start]):::startNode | ||
| B[Load Last Sync Timestamp]:::processNode | ||
| C[Fetch HubSpot Contacts]:::processNode | ||
| D{Contact Filter Enabled?}:::decisionNode | ||
| E[Filter Contacts]:::processNode | ||
| F[Determine Lifecycle Stage]:::processNode | ||
| G[Select Target Google Sheet]:::processNode | ||
| SM{Sync Mode?}:::decisionNode | ||
| REP[Clear Sheet Data]:::processNode | ||
| H[Check Existing Row by Email]:::processNode | ||
| I[Write Contact Row]:::processNode | ||
| J[Update Last Sync Timestamp]:::processNode | ||
| K([End]):::endNode | ||
|
|
||
| A --> B | ||
| B --> C | ||
| C --> D | ||
| D -->|Yes| E | ||
| D -->|No| F | ||
| E --> F | ||
| F --> G | ||
| G --> SM | ||
| SM -->|replace| REP | ||
| REP --> I | ||
| SM -->|append| I | ||
| SM -->|upsert| H | ||
| H --> I | ||
| I --> J | ||
| J --> K | ||
|
|
||
| classDef startNode fill:#4CAF50,color:#fff | ||
| classDef endNode fill:#F44336,color:#fff | ||
| classDef processNode fill:#2196F3,color:#fff | ||
| classDef decisionNode fill:#FF9800,color:#fff | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| # HubSpot Contacts to Google Sheets | ||
|
|
||
| ## What It Does | ||
|
|
||
| - Fetches contacts from HubSpot CRM Contacts API | ||
| - Routes contacts to sheet tabs based on lifecycle stage | ||
| - Upserts rows using email as the unique key | ||
| - Supports incremental sync and optional filtering | ||
| - Runs once per execution (scheduling is handled externally) | ||
|
|
||
| <details> | ||
| <summary>HubSpot Setup</summary> | ||
|
|
||
| 1. Sign in to HubSpot. | ||
| 2. Go to Settings > Integrations > Legacy Apps. | ||
| 3. Create a Legacy app and enable the scope `crm.objects.contacts.read`. | ||
| 4. Obtain the generated access token. | ||
|
Comment on lines
+14
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Align the HubSpot setup steps with the auth flow this sample is meant to support. Lines 15-17 still direct users to Legacy Apps, but the sample is described as using a Private App token. Please update these steps—and the matching token description in 🤖 Prompt for AI Agents |
||
|
|
||
| </details> | ||
|
|
||
| <details> | ||
| <summary>Google Sheets Setup Guide</summary> | ||
|
|
||
| 1. A Google Cloud project with Google Sheets API enabled | ||
| 2. OAuth2 credentials: | ||
| - Client ID | ||
| - Client Secret | ||
| - Refresh Token | ||
| 3. Scopes Required | ||
| - `https://www.googleapis.com/auth/drive` | ||
| - `https://www.googleapis.com/auth/spreadsheets` | ||
| This integration uses refresh token flow for auth. [Learn how to Develop on Google Workspace](https://developers.google.com/workspace/guides/get-started). | ||
|
|
||
| </details> | ||
|
|
||
| <details> | ||
| <summary>Spreadsheet Setup</summary> | ||
|
|
||
| 1. Create a Google Spreadsheet. | ||
| 2. Copy the spreadsheet ID from `https://docs.google.com/spreadsheets/d/<spreadsheetId>/edit`. | ||
| 3. Use that value as `spreadsheetId`. | ||
| 4. The integration creates missing lifecycle-stage tabs automatically. | ||
|
|
||
| Default sheet mapping: | ||
|
|
||
| - Subscriber -> `Subscribers` | ||
| - Lead -> `Leads` | ||
| - Marketing Qualified Lead -> `MQLs` | ||
| - Sales Qualified Lead -> `SQLs` | ||
| - Opportunity -> `Opportunities` | ||
| - Customer -> `Customers` | ||
| - Evangelist -> `Evangelists` | ||
| - Other -> `Others` | ||
| - Unrecognized/empty -> `Sheet1` | ||
|
|
||
| </details> | ||
|
|
||
| <details> | ||
| <summary>Additional Configuration</summary> | ||
|
|
||
| - `fields`: HubSpot properties exported as columns | ||
| - `syncMode`: `upsert` (default), `append`, or `replace` | ||
| - `maxRows`: max contacts per run (`0` means unlimited) | ||
| - `lastSyncTimestamp`: optional initial checkpoint | ||
| - `contactFilterProperty` / `contactFilterValue`: optional HubSpot filter | ||
|
|
||
| </details> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # Ballerina generates this directory during the compilation of a package. | ||
| # It contains compiler-generated artifacts and the final executable if this is an application package. | ||
| target/ | ||
|
|
||
| # Ballerina maintains the compiler-generated source code here. | ||
| # Remove this if you want to commit generated sources. | ||
| generated/ | ||
|
|
||
| # Contains configuration values used during development time. | ||
| # See https://ballerina.io/learn/provide-values-to-configurable-variables/ for more details. | ||
| Config.toml | ||
|
|
||
| # Dependency definitions | ||
| Dependencies.toml | ||
|
|
||
| last_sync_timestamp.txt | ||
|
|
||
| .DS_Store | ||
|
|
||
| .vscode |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| [package] | ||
| org = "wso2" | ||
| name = "hubspot_contacts_to_google_sheet" | ||
| version = "0.1.0" | ||
| title = "hubspot-contacts-to-google-sheet" | ||
| distribution = "2201.13.1" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # HubSpot API | ||
| hubspotAccessToken = "<your-hubspot-legacy-app-token>" | ||
|
|
||
| # Google OAuth2 | ||
| googleClientId = "<your-google-client-id>" | ||
| googleClientSecret = "<your-google-client-secret>" | ||
| googleRefreshToken = "<your-google-refresh-token>" | ||
|
|
||
| # Google Spreadsheet target | ||
| spreadsheetId = "<your-google-spreadsheet-id>" | ||
|
|
||
| # Lifecycle routing sheets | ||
| # Each HubSpot lifecycle stage routes to its own sheet by default. | ||
| # To merge multiple stages into one sheet, set them to the same name. | ||
| # To send all contacts to a single sheet, set all names to the same value. | ||
| subscriberSheetName = "Subscribers" | ||
| leadSheetName = "Leads" | ||
| marketingqualifiedleadSheetName = "MQLs" | ||
| salesqualifiedleadSheetName = "SQLs" | ||
| opportunitySheetName = "Opportunities" | ||
| customerSheetName = "Customers" | ||
| evangelistSheetName = "Evangelists" | ||
| otherSheetName = "Others" | ||
| defaultSheetName = "Sheet1" | ||
|
|
||
| # Limits | ||
| maxRows = 0 | ||
|
|
||
| # Sync mode | ||
| # "upsert" - Update existing row if email matches, insert if not (default) | ||
| # "append" - Always insert a new row, never check for duplicates | ||
| # "replace" - Clear the sheet first, then write all contacts fresh | ||
| syncMode = "upsert" | ||
|
|
||
| # Optional contact filter (leave empty to export all) | ||
| contactFilterProperty = "" | ||
| contactFilterValue = "" | ||
|
|
||
| # Example filter: | ||
| # contactFilterProperty = "lifecyclestage" | ||
| # contactFilterValue = "customer" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tighten validation for
maxRowsandsyncMode.Right now negative
maxRowsvalues are accepted and behave like “unlimited” at runtime, and any unknownsyncModesilently falls back to the upsert branch. Adding schema constraints here makes those misconfigurations fail fast instead of changing sync behavior unexpectedly.Suggested schema update
"maxRows": { "type": "integer", + "minimum": 0, "description": "Maximum number of contacts to process per incremental sync run. Set to 0 for no limit." }, "syncMode": { "type": "string", + "enum": ["upsert", "append", "replace"], "description": "Sync strategy: 'upsert' (default) updates existing rows and inserts new ones; 'append' always inserts; 'replace' clears the sheet then inserts all contacts." }📝 Committable suggestion
🤖 Prompt for AI Agents