The real problem
Why this is harder than it looks
HubSpot's API documentation is genuinely good. The authorization flow is standard OAuth 2.0. Most developers expect to spend an afternoon on it. The problems start when you go beyond a single-user prototype and try to run this for actual customers.
HubSpot access tokens expire after 30 minutes. That's not a typo — thirty minutes. In production, where an agent might be processing a background workflow or acting on a trigger that fires at midnight, a stale token means a silent failure mid-operation unless you've built proactive refresh logic that runs ahead of expiry. Most teams build the refresh only after they've been burned by the 401 in production.
Then there's HubSpot's scope model. Every CRM object — contacts, companies, deals — has separate read and write scopes. Schema access is another scope entirely. HubSpot enforces an exact match: the scopes your agent requests at authorization time must match exactly what you declared in your developer app settings. A single mismatch blocks the entire install with a cryptic error that users can't resolve themselves. As your agent's capabilities grow and you add new tools, you'll need users to re-authorize — and managing that re-authorization gracefully across a multi-tenant user base is a non-trivial problem.
Finally, HubSpot requires you to register your own Public App to get a Client ID and Secret. Each of your customers authorizes through your app, and you're responsible for keeping those credentials secure, rotating them when needed, and wiring up the redirect URI exactly as registered.
Scalekit handles the token lifecycle, scope configuration, credential storage, and revocation detection. Your agent code calls a tool by name. The plumbing is invisible.
Capabilities
What your agent can do with HubSpot
Once connected, your agent has 11 pre-built tools covering the core HubSpot CRM object model:
- Search contacts, companies, and deals: full-text search with filter groups and pagination across all major CRM objects
- Create and update contacts: with email, name, job title, lifecycle stage, and lead status
- Create and update deals: with pipeline, deal stage, amount, close date, and priority
- Create and retrieve companies: with industry, revenue, headcount, domain, and location metadata
- List contacts with cursor pagination: including archived records, with field selection
Setup context
What we're building
This guide connects a sales assistant agent to HubSpot — helping reps search contacts, manage deals, and update CRM records without leaving your product.
🤖
Example agent
Sales assistant managing HubSpot pipeline on behalf of each rep
🔐
Auth model
B2B SaaS — each rep connects their own HubSpot portal. identifier = your user ID
Setup
1 Setup: One SDK, One credential
Install the Scalekit SDK. The only credential your application manages is the Scalekit API key — no HubSpot secrets, no user tokens, nothing belonging to your customers.
pip install scalekit-sdk-python
import scalekit.client
import os
from dotenv import load_dotenv
load_dotenv()
scalekit = scalekit.client.ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENV_URL"),
)
actions = scalekit.actions
Connected Accounts
2 Per-User Auth: Creating connected accounts
Each rep gets their own Connected Account, giving them a dedicated auth context. The identifier is any unique string from your system — a UUID, email, whatever you use internally.
response = actions.get_or_create_connected_account(
connection_name="hubspot",
identifier="user_hs_456" # your internal user ID
)
connected_account = response.connected_account
print(f"Status: {connected_account.status}")
# Status: PENDING — user hasn't authorized yet
This call is idempotent — safe to call on every session start. Returns the existing account if one already exists.
Authorization Flow
3 The authorization flow
The rep authorizes your agent once. Scalekit generates the OAuth URL with correct scopes, redirect handling, and state pre-configured. After approval, you never see the token.
if connected_account.status != "ACTIVE":
link = actions.get_authorization_link(
connection_name="hubspot",
identifier="user_hs_456"
)
# Redirect user → HubSpot consent screen
# Scalekit captures the token on callback
return redirect(link.link)
Token management is automatic
After the user approves, Scalekit stores encrypted tokens and the connected account moves to ACTIVE. HubSpot
access tokens expire after 30 minutes — Scalekit refreshes them before expiry. If a rep uninstalls your app
from their HubSpot portal, the account moves to REVOKED — no silent failures. Check account.status before
critical operations.
BYOC is required for HubSpot
Unlike some connectors, HubSpot requires you to register your own Public App and supply your own Client ID
and Client Secret. Create your app at developers.hubspot.com, add the Scalekit redirect URI under Auth
settings, configure your scopes, then paste the credentials into the Scalekit dashboard. Token management
stays fully handled.
Already have credentials?
Calling HubSpot
4 Calling HubSpot: What your agent writes
With the connected account active, your agent calls Salesforce actions using execute_tool. Name the tool, pass parameters. Scalekit handles token retrieval, request construction, and response parsing.
Search contacts
Full-text search across all contacts. Use filterGroups for advanced property-based filtering.
result = actions.execute_tool(
identifier="user_hs_456",
tool_name="hubspot_contacts_search",
tool_input={
"query": "acme corp",
"properties": "firstname,lastname,email,jobtitle,lifecyclestage",
"limit": 10
}
)
# { "results": [ { "id": "...", "properties": { "firstname": "Jane", ... } }, ... ] }
Create a contact
Email is the unique identifier and the only required field. All other properties are optional.
result = actions.execute_tool(
identifier="user_hs_456",
tool_name="hubspot_contact_create",
tool_input={
"email": "jane@acmecorp.com",
"firstname": "Jane",
"lastname": "Smith",
"jobtitle": "VP of Engineering",
"company": "Acme Corp",
"lifecyclestage": "lead"
}
)
Create a deal
dealname, amount, and dealstage are required. Pipeline defaults to the account's primary pipeline if not specified.
result = actions.execute_tool(
identifier="user_hs_456",
tool_name="hubspot_deal_create",
tool_input={
"dealname": "Acme Corp — Enterprise 2026",
"amount": 75000,
"dealstage": "presentationscheduled",
"pipeline": "default",
"closedate": "2026-06-30",
"hs_priority": "HIGH"
}
)
Search deals with filter groups
For precise queries — deals above a certain amount in a specific stage — pass filterGroups as a JSON string.
import json
result = actions.execute_tool(
identifier="user_hs_456",
tool_name="hubspot_deals_search",
tool_input={
"filterGroups": json.dumps([{
"filters": [
{"propertyName": "dealstage", "operator": "EQ", "value": "presentationscheduled"},
{"propertyName": "amount", "operator": "GT", "value": "50000"}
]
}]),
"properties": "dealname,amount,dealstage,closedate,hs_priority",
"limit": 20
}
)
Framework wiring
5 Wiring into your agent framework
Scalekit's tools load directly into LangChain. The agent decides what to call; Scalekit handles auth on every invocation. No token plumbing in your agent logic.
from langchain_anthropic import ChatAnthropic
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from scalekit.langchain import get_tools
hs_tools = get_tools(
connection_name="hubspot",
identifier="user_hs_456"
)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a CRM assistant. Use the available tools to help manage HubSpot contacts, companies, and deals."),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-6"), hs_tools, prompt)
result = AgentExecutor(agent=agent, tools=hs_tools).invoke({
"input": "Find all high-priority deals over $50k in presentation stage and summarize the pipeline"
})
Other frameworks supported
Tool reference
All 11 HubSpot tools
Grouped by object and capability. Your agent calls tools by name — no API wrappers to write.
Create a contact with email (required), name, phone, job title, company, and lifecycle stage
Retrieve a specific contact by ID with field selection
Update any contact property by contact ID — name, email, lifecycle stage, lead status, and more
List contacts with cursor-based pagination, field selection, and optional archived record inclusion
Full-text search across contacts with optional filter groups for property-based queries
Create a company with name (required), domain, industry, revenue, headcount, and location
Retrieve a specific company by ID with field selection
Full-text search across companies with optional filter groups and pagination
Create a deal with name, amount, and stage (all required). Supports pipeline, close date, type, and priority
Update any deal property by deal ID — stage, amount, close date, pipeline, and priority
Full-text search across deals with filter groups for stage, amount, priority, and custom properties
Connector notes
HubSpot-specific behavior
BYOC is required — no managed app available
Unlike some Scalekit connectors, HubSpot does not provide a managed app for getting started. You must
register a Public App at developers.hubspot.com, copy the Scalekit redirect URI into your app's Auth
settings, and paste your Client ID and Client Secret into the Scalekit dashboard before the authorization
flow will work.
Scope mismatches block installation silently
HubSpot enforces an exact match between the scopes declared in your Public App settings and the scopes
requested at authorization time. A single mismatch blocks the install with an error your users cannot
resolve themselves. Ensure the scope set in the Scalekit dashboard matches your declared app scopes exactly
— and plan for re-authorization when you add new tool capabilities that require new scopes.
Access tokens expire every 30 minutes
HubSpot's access tokens have a 30-minute lifetime — shorter than most OAuth providers. Scalekit refreshes
them proactively before expiry, so your agent never hits a stale token mid-operation. This is handled
automatically; no code changes required on your end.
Infrastructure decision
Why not build this yourself
The HubSpot OAuth flow is documented. Token storage isn't technically hard. But here's what you're actually signing up for:
PROBLEM 01
30-minute access token expiry means refresh logic must be proactive, not reactive — a failed call mid-workflow is not a graceful failure
PROBLEM 02
Scope mismatch enforcement that blocks installs silently — and forces re-authorization across all users whenever you add new agent capabilities
PROBLEM 03
Per-user token isolation across a multi-tenant system — one rep's HubSpot portal credentials must never be accessible to another
PROBLEM 04
Revocation detection when a rep uninstalls your app from their HubSpot portal — and graceful handling so the agent stops making calls immediately
That's one connector. Your agent product will eventually need Salesforce, Gmail, Slack, Notion, and whatever else your customers ask for. Each has its own OAuth quirks and failure modes.
Scalekit maintains every connector. You maintain none of them.