0Pricing
AI Agents · Lesson

CRM Integration: Salesforce and HubSpot

Fetching customer history, logging interactions, and updating CRM records.

CRM Integration: Salesforce and HubSpot is a free AI Agents lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Agents learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why CRM Integration Matters

A customer service agent that cannot see the customer's history is flying blind. CRM systems like Salesforce and HubSpot hold the full customer record: past tickets, purchases, subscription tier, and account notes.

Integrating with the CRM lets the agent personalize responses and resolve issues faster.

Connecting to Salesforce with simple-salesforce

simple-salesforce is the most popular Python library for the Salesforce REST API. Connect using a username/password/security-token or an OAuth connected app flow.

from simple_salesforce import Salesforce

sf = Salesforce(
    username='agent@company.com',
    password='YOUR_SF_PASSWORD',
    security_token='YOUR_SF_TOKEN',
    domain='login'   # use 'test' for sandbox
)
print('Connected to Salesforce org:', sf.base_url)

SOQL Queries: Fetching Customer Records

Salesforce Object Query Language (SOQL) is SQL-like but operates on Salesforce objects. Use it to look up a contact by email and retrieve their account details.

def get_customer_by_email(sf, email: str) -> dict | None:
    query = (
        f"SELECT Id, FirstName, LastName, Email, AccountId, "
        f"Account.Name, Account.Type "
        f"FROM Contact "
        f"WHERE Email = '{email}' "
        f"LIMIT 1"
    )
    result = sf.query(query)
    if result['totalSize'] == 0:
        return None
    record = result['records'][0]
    return {
        'contact_id': record['Id'],
        'name': f"{record['FirstName']} {record['LastName']}",
        'account_id': record['AccountId'],
        'account_name': record['Account']['Name']
    }

if __name__ == '__main__':
    class FakeSF:
        def query(self, query):
            return {
                'totalSize': 1,
                'records': [{
                    'Id': '003ABC', 'FirstName': 'Jane', 'LastName': 'Doe',
                    'AccountId': '001XYZ',
                    'Account': {'Name': 'Acme Corp', 'Type': 'Customer'},
                }],
            }
    customer = get_customer_by_email(FakeSF(), 'jane@acme.com')
    print(f"Found customer: {customer['name']} at {customer['account_name']}")

Fetching Support Case History from Salesforce

Query the Case object to retrieve the customer's previous support tickets. Include case status and resolution so the agent can avoid asking the same questions twice.

def get_case_history(sf, contact_id: str, limit: int = 10) -> list:
    query = (
        f"SELECT Id, CaseNumber, Subject, Status, "
        f"CreatedDate, ClosedDate, Resolution__c "
        f"FROM Case "
        f"WHERE ContactId = '{contact_id}' "
        f"ORDER BY CreatedDate DESC "
        f"LIMIT {limit}"
    )
    result = sf.query(query)
    return [
        {
            'case_number': r['CaseNumber'],
            'subject': r['Subject'],
            'status': r['Status'],
            'created': r['CreatedDate'][:10],
            'resolution': r.get('Resolution__c')
        }
        for r in result['records']
    ]

if __name__ == '__main__':
    class FakeSF:
        def query(self, query):
            return {'records': [{
                'Id': '500A', 'CaseNumber': '00001234', 'Subject': 'Login issue',
                'Status': 'Closed', 'CreatedDate': '2026-07-01T10:00:00Z',
                'ClosedDate': '2026-07-02T09:00:00Z', 'Resolution__c': 'Reset password',
            }]}
    for case in get_case_history(FakeSF(), '003ABC'):
        print(f"Case {case['case_number']}: {case['subject']} ({case['status']})")

Creating a New Case in Salesforce

When the agent creates a new support ticket from a conversation, insert a new Case record. Link it to the contact and account so it appears in the customer's timeline.

def create_case(sf, contact_id: str, account_id: str,
                subject: str, description: str, priority: str = 'Medium') -> str:
    result = sf.Case.create({
        'ContactId': contact_id,
        'AccountId': account_id,
        'Subject': subject,
        'Description': description,
        'Priority': priority,
        'Status': 'New',
        'Origin': 'AI Agent'
    })
    case_id = result['id']
    print(f'Created Case: {case_id}')
    return case_id

if __name__ == '__main__':
    class FakeCase:
        def create(self, data):
            return {'id': '500NEW01'}
    class FakeSF:
        Case = FakeCase()

    create_case(FakeSF(), '003ABC', '001XYZ', 'Cannot reset password', 'Customer locked out after 3 attempts')

Connecting to HubSpot

The hubspot-api-client library wraps HubSpot's REST API. Authenticate with a private app access token (no OAuth needed for server-to-server integrations).

from hubspot import HubSpot
from hubspot.crm.contacts import SimplePublicObjectInput

client = HubSpot(access_token='YOUR_HUBSPOT_PRIVATE_APP_TOKEN')
print('HubSpot client ready')

Searching for a HubSpot Contact

Use the Contacts search API to find a customer by email. The response includes all contact properties including lifecycle stage, deal associations, and custom fields.

from hubspot.crm.contacts import PublicObjectSearchRequest

def get_hubspot_contact(client, email: str) -> dict | None:
    search_request = PublicObjectSearchRequest(
        filter_groups=[{
            'filters': [{
                'propertyName': 'email',
                'operator': 'EQ',
                'value': email
            }]
        }],
        properties=['firstname', 'lastname', 'email',
                    'lifecyclestage', 'hs_object_id']
    )
    response = client.crm.contacts.search_api.do_search(
        public_object_search_request=search_request
    )
    if response.total == 0:
        return None
    contact = response.results[0]
    return {'id': contact.id, **contact.properties}

Fetching HubSpot Ticket History

HubSpot stores support tickets in its tickets object. Retrieve associated tickets for a contact using the associations API.

def get_hubspot_tickets(client, contact_id: str) -> list:
    assoc = client.crm.contacts.associations_api.get_all(
        contact_id,
        to_object_type='tickets'
    )
    ticket_ids = [r.id for r in assoc.results]
    if not ticket_ids:
        return []

    tickets = []
    for tid in ticket_ids[:10]:  # limit to 10 most recent
        ticket = client.crm.tickets.basic_api.get_by_id(
            tid,
            properties=['subject', 'hs_ticket_priority',
                        'hs_pipeline_stage', 'createdate']
        )
        tickets.append({
            'id': tid,
            'subject': ticket.properties.get('subject'),
            'priority': ticket.properties.get('hs_ticket_priority'),
            'stage': ticket.properties.get('hs_pipeline_stage')
        })
    return tickets

if __name__ == '__main__':
    from types import SimpleNamespace

    class AssocApi:
        def get_all(self, contact_id, to_object_type):
            return SimpleNamespace(results=[SimpleNamespace(id='t1'), SimpleNamespace(id='t2')])

    class TicketsBasicApi:
        _data = {
            't1': {'subject': 'Cannot login', 'hs_ticket_priority': 'HIGH', 'hs_pipeline_stage': '1'},
            't2': {'subject': 'Refund request', 'hs_ticket_priority': 'MEDIUM', 'hs_pipeline_stage': '2'},
        }
        def get_by_id(self, tid, properties):
            return SimpleNamespace(properties=self._data[tid])

    fake_client = SimpleNamespace(crm=SimpleNamespace(
        contacts=SimpleNamespace(associations_api=AssocApi()),
        tickets=SimpleNamespace(basic_api=TicketsBasicApi()),
    ))

    for t in get_hubspot_tickets(fake_client, 'contact_1'):
        print(f"Ticket {t['id']}: {t['subject']} [{t['priority']}]")

Creating a HubSpot Ticket

Insert a new ticket and associate it with the contact in a single workflow. This keeps the customer's CRM record up to date automatically as the agent handles requests.

from hubspot.crm.tickets import SimplePublicObjectInputForCreate
from hubspot.crm.associations.v4.models import AssociationSpec

def create_hubspot_ticket(client, contact_id: str,
                          subject: str, description: str) -> str:
    ticket_input = SimplePublicObjectInputForCreate(
        properties={
            'subject': subject,
            'content': description,
            'hs_ticket_priority': 'MEDIUM',
            'hs_pipeline': 'support_pipeline',
            'hs_pipeline_stage': 'new'
        },
        associations=[{
            'types': [AssociationSpec(association_category='HUBSPOT_DEFINED',
                                     association_type_id=16)],
            'to': {'id': contact_id}
        }]
    )
    ticket = client.crm.tickets.basic_api.create(
        simple_public_object_input_for_create=ticket_input
    )
    return ticket.id

Updating a Contact Record

After a successful resolution, update the contact's record with notes and lifecycle stage changes. This keeps the CRM current without manual agent effort.

def update_hubspot_contact(client, contact_id: str, updates: dict) -> None:
    client.crm.contacts.basic_api.update(
        contact_id=contact_id,
        simple_public_object_input=SimplePublicObjectInput(
            properties=updates
        )
    )

# After resolving a billing issue:
update_hubspot_contact(client, 'CONTACT_123', {
    'hs_lead_status': 'CONNECTED',
    'notes_last_contacted': 'Resolved billing duplicate charge issue via AI agent.'
})

Unified CRM Context for the Agent

Build a single function that fetches customer context from whichever CRM is configured. The agent calls this at the start of each conversation to personalize its responses.

def get_customer_context(email: str, crm: str = 'salesforce') -> dict:
    if crm == 'salesforce':
        customer = get_customer_by_email(sf, email)
        if not customer:
            return {'found': False}
        cases = get_case_history(sf, customer['contact_id'])
        return {'found': True, 'customer': customer, 'history': cases}
    elif crm == 'hubspot':
        customer = get_hubspot_contact(client, email)
        if not customer:
            return {'found': False}
        tickets = get_hubspot_tickets(client, customer['id'])
        return {'found': True, 'customer': customer, 'history': tickets}
    return {'found': False, 'error': 'Unknown CRM'}

Which Salesforce query language is used to retrieve records via the simple-salesforce library?

Salesforce uses its own query language to retrieve records. Knowing this is essential for building agent tools that look up customer history.

CRM Integration Recap

Integrating with Salesforce (simple-salesforce + SOQL) or HubSpot (hubspot-api-client) gives your customer service agent full customer context: history, cases, tickets, and account data.

Always build a unified get_customer_context() function so the agent prompt is enriched with relevant history before generating a response.

Frequently asked questions

Is the “CRM Integration: Salesforce and HubSpot” lesson free?

Yes — the full text of “CRM Integration: Salesforce and HubSpot” is free to read here on the web, and the AI Agents course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Agents course, upgrade to CoddyKit PRO.

What will I learn in “CRM Integration: Salesforce and HubSpot”?

Fetching customer history, logging interactions, and updating CRM records. You practise AI Agents with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start AI Agents?

No prior experience is required. AI Agents on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “CRM Integration: Salesforce and HubSpot” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this AI Agents lesson?

Yes. Every AI Agents lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Ticket Routing and Escalation Logic
  2. CRM Integration: Salesforce and HubSpot
  3. Human Handoff Protocols
  4. Customer Context and History Management
← Back to AI Agents