hubspot-crm

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

HubSpot CRM Integration

HubSpot CRM 集成

Sync contacts and lists to HubSpot using the REST API.
使用REST API将联系人与列表同步到HubSpot。

Environment Variables

环境变量

bash
HUBSPOT_API_TOKEN="pat-na1-..."  # Private App token from HubSpot
bash
HUBSPOT_API_TOKEN="pat-na1-..."  # 来自HubSpot的私有应用令牌

Quick Start

快速开始

python
import os
import json
from urllib.request import Request, urlopen
from urllib.error import HTTPError

class HubSpotClient:
    """Simple HubSpot API client."""

    def __init__(self):
        self.token = os.environ.get('HUBSPOT_API_TOKEN')
        if not self.token:
            raise ValueError("HUBSPOT_API_TOKEN environment variable not set")
        self.base_url = "https://api.hubapi.com"

    def _request(self, method: str, endpoint: str, data: dict = None) -> dict:
        url = f"{self.base_url}{endpoint}"
        headers = {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json"
        }
        body = json.dumps(data).encode('utf-8') if data else None
        request = Request(url, data=body, headers=headers, method=method)

        with urlopen(request, timeout=30) as response:
            return json.loads(response.read().decode('utf-8'))
python
import os
import json
from urllib.request import Request, urlopen
from urllib.error import HTTPError

class HubSpotClient:
    """简单的HubSpot API客户端。"""

    def __init__(self):
        self.token = os.environ.get('HUBSPOT_API_TOKEN')
        if not self.token:
            raise ValueError("未设置HUBSPOT_API_TOKEN环境变量")
        self.base_url = "https://api.hubapi.com"

    def _request(self, method: str, endpoint: str, data: dict = None) -> dict:
        url = f"{self.base_url}{endpoint}"
        headers = {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json"
        }
        body = json.dumps(data).encode('utf-8') if data else None
        request = Request(url, data=body, headers=headers, method=method)

        with urlopen(request, timeout=30) as response:
            return json.loads(response.read().decode('utf-8'))

Create Static List

创建静态列表

python
def create_static_list(client, name: str) -> str:
    """Create a static list for contacts. Returns list ID."""
    payload = {
        "name": name,
        "objectTypeId": "0-1",  # REQUIRED: 0-1 = contacts
        "processingType": "MANUAL"
    }

    result = client._request("POST", "/crm/v3/lists", payload)
    # Response is nested: {"list": {"listId": "..."}}
    list_data = result.get("list", result)
    list_id = list_data.get("listId")

    print(f"✅ Created list: {name} (ID: {list_id})")
    return list_id
python
def create_static_list(client, name: str) -> str:
    """为联系人创建静态列表。返回列表ID。"""
    payload = {
        "name": name,
        "objectTypeId": "0-1",  # 必填项:0-1 = 联系人
        "processingType": "MANUAL"
    }

    result = client._request("POST", "/crm/v3/lists", payload)
    # 响应为嵌套结构:{"list": {"listId": "..."}}
    list_data = result.get("list", result)
    list_id = list_data.get("listId")

    print(f"✅ 已创建列表: {name} (ID: {list_id})")
    return list_id

Search Contact by Email

通过邮箱搜索联系人

python
def search_contact(client, email: str) -> str | None:
    """Find contact by email. Returns contact ID or None."""
    payload = {
        "filterGroups": [{
            "filters": [{
                "propertyName": "email",
                "operator": "EQ",
                "value": email
            }]
        }],
        "properties": ["email"],
        "limit": 1
    }

    result = client._request("POST", "/crm/v3/objects/contacts/search", payload)
    results = result.get("results", [])
    return results[0]["id"] if results else None
python
def search_contact(client, email: str) -> str | None:
    """通过邮箱查找联系人。返回联系人ID或None。"""
    payload = {
        "filterGroups": [{
            "filters": [{
                "propertyName": "email",
                "operator": "EQ",
                "value": email
            }]
        }],
        "properties": ["email"],
        "limit": 1
    }

    result = client._request("POST", "/crm/v3/objects/contacts/search", payload)
    results = result.get("results", [])
    return results[0]["id"] if results else None

Create Contact

创建联系人

python
def create_contact(client, email: str, firstname: str = None, lastname: str = None) -> str:
    """Create a new contact with email only (vanilla upload).

    Only uses standard HubSpot properties (email, firstname, lastname)
    to avoid errors from missing custom properties in the target account.
    """
    properties = {"email": email}
    if firstname:
        properties["firstname"] = firstname
    if lastname:
        properties["lastname"] = lastname

    result = client._request("POST", "/crm/v3/objects/contacts", {"properties": properties})
    return result["id"]
Important: Do NOT pass arbitrary CSV columns as properties. HubSpot will reject any property names that don't exist in the target account. Only use standard fields (email, firstname, lastname) unless you've confirmed custom properties exist.
python
def create_contact(client, email: str, firstname: str = None, lastname: str = None) -> str:
    """仅使用邮箱创建新联系人(基础上传)。

    仅使用HubSpot标准属性(email、firstname、lastname),
    避免因目标账户中缺少自定义属性而导致错误。
    """
    properties = {"email": email}
    if firstname:
        properties["firstname"] = firstname
    if lastname:
        properties["lastname"] = lastname

    result = client._request("POST", "/crm/v3/objects/contacts", {"properties": properties})
    return result["id"]
重要提示:请勿将任意CSV列作为属性传入。如果属性名称在目标账户中不存在,HubSpot会拒绝请求。除非已确认自定义属性存在,否则仅使用标准字段(邮箱、名字、姓氏)。

Add Contacts to List

将联系人添加到列表

python
def add_to_list(client, list_id: str, contact_ids: list[str]):
    """Add contacts to a static list. Batches in groups of 100."""
    endpoint = f"/crm/v3/lists/{list_id}/memberships/add"

    batch_size = 100
    total_added = 0

    for i in range(0, len(contact_ids), batch_size):
        batch = contact_ids[i:i + batch_size]
        # IMPORTANT: Payload is a simple array, NOT {"recordIdsToAdd": [...]}
        result = client._request("PUT", endpoint, batch)
        added = len(result.get("recordsIdsAdded", []))
        total_added += added
        print(f"  Added batch: {added} contacts")

    print(f"✅ Added {total_added} total contacts to list")
python
def add_to_list(client, list_id: str, contact_ids: list[str]):
    """将联系人添加到静态列表。按每100个一组分批处理。"""
    endpoint = f"/crm/v3/lists/{list_id}/memberships/add"

    batch_size = 100
    total_added = 0

    for i in range(0, len(contact_ids), batch_size):
        batch = contact_ids[i:i + batch_size]
        # 重要:请求体为简单数组,而非 {"recordIdsToAdd": [...]}
        result = client._request("PUT", endpoint, batch)
        added = len(result.get("recordsIdsAdded", []))
        total_added += added
        print(f"  已添加批次: {added} 个联系人")

    print(f"✅ 已将总计 {total_added} 个联系人添加到列表")

Full Upload Flow

完整上传流程

python
def upload_users_to_hubspot(emails: list[str], list_name: str) -> str:
    """Upload a list of email addresses to HubSpot."""
    client = HubSpotClient()

    # Create list
    list_id = create_static_list(client, list_name)

    # Find or create contacts
    contact_ids = []
    for email in emails:
        contact_id = search_contact(client, email)
        if not contact_id:
            contact_id = create_contact(client, email)
        contact_ids.append(contact_id)

    # Add to list
    add_to_list(client, list_id, contact_ids)

    print(f"\n✅ Complete!")
    print(f"   List: https://app.hubspot.com/contacts/lists/{list_id}")

    return list_id
python
def upload_users_to_hubspot(emails: list[str], list_name: str) -> str:
    """将邮箱地址列表上传到HubSpot。"""
    client = HubSpotClient()

    # 创建列表
    list_id = create_static_list(client, list_name)

    # 查找或创建联系人
    contact_ids = []
    for email in emails:
        contact_id = search_contact(client, email)
        if not contact_id:
            contact_id = create_contact(client, email)
        contact_ids.append(contact_id)

    # 添加到列表
    add_to_list(client, list_id, contact_ids)

    print(f"\n✅ 完成!")
    print(f"   列表地址: https://app.hubspot.com/contacts/lists/{list_id}")

    return list_id

Usage Example

使用集成脚本

python
undefined
对于CSV文件,使用提供的脚本:
bash
.venv/bin/python demos/04-trial-to-paid/scripts/hubspot_integration.py \
  path/to/users.csv \
  "List Name Here"
CSV文件必须包含
email
列。

Upload at-risk users from analysis

API注意事项

at_risk_emails = [ "user_001@demo.reformapp.com", "user_002@demo.reformapp.com", "user_003@demo.reformapp.com" ]
list_id = upload_users_to_hubspot( emails=at_risk_emails, list_name="At-Risk Trial Users - Dec 2024" )
undefined
  1. 创建列表需要
    objectTypeId
    :对于联系人,务必包含
    "objectTypeId": "0-1"
  2. 响应为嵌套结构:列表ID位于
    result["list"]["listId"]
    ,而非
    result["listId"]
  3. 添加到列表的请求体格式:使用简单数组
    ["id1", "id2"]
    ,而非
    {"recordIdsToAdd": [...]}
  4. 联系人ID为字符串类型:即使看起来是数字,也要将其视为字符串处理
  5. 批次限制:添加联系人时,每批最多100个

Using the Integration Script

错误处理

For CSV files, use the provided script:
bash
.venv/bin/python demos/04-trial-to-paid/scripts/hubspot_integration.py \
  path/to/users.csv \
  "List Name Here"
The CSV must have an
email
column.
python
from urllib.error import HTTPError

try:
    result = client._request("POST", endpoint, payload)
except HTTPError as e:
    error_body = e.read().decode('utf-8')
    print(f"HubSpot API错误: {e.code}")
    print(f"  {error_body}")

API Gotchas

获取HubSpot私有应用令牌

  1. List creation requires
    objectTypeId
    : Always include
    "objectTypeId": "0-1"
    for contacts
  2. Response is nested: List ID is at
    result["list"]["listId"]
    , not
    result["listId"]
  3. Add-to-list payload format: Use simple array
    ["id1", "id2"]
    , NOT
    {"recordIdsToAdd": [...]}
  4. Contact IDs are strings: Even though they look numeric, treat them as strings
  5. Batch limit: Add contacts in batches of 100 max
  1. 进入HubSpot的设置(齿轮图标)
  2. 导航到集成 > 私有应用
  3. 点击创建私有应用
  4. 为应用命名(例如:"AI Agent Integration")
  5. 权限范围下,启用:
    • crm.lists.read
    • crm.lists.write
    • crm.objects.contacts.read
    • crm.objects.contacts.write
  6. 点击创建应用并复制令牌
  7. 将其设置为
    HUBSPOT_API_TOKEN
    环境变量

Error Handling

python
from urllib.error import HTTPError

try:
    result = client._request("POST", endpoint, payload)
except HTTPError as e:
    error_body = e.read().decode('utf-8')
    print(f"HubSpot API error: {e.code}")
    print(f"  {error_body}")

Getting Your HubSpot Private App Token

  1. Go to Settings (gear icon) in HubSpot
  2. Navigate to Integrations > Private Apps
  3. Click Create a private app
  4. Give it a name (e.g., "AI Agent Integration")
  5. Under Scopes, enable:
    • crm.lists.read
    • crm.lists.write
    • crm.objects.contacts.read
    • crm.objects.contacts.write
  6. Click Create app and copy the token
  7. Set as
    HUBSPOT_API_TOKEN
    environment variable