The CRM that grew into a customer platform โ sales, marketing, service, CMS, and operations on one set of objects. The 2nth skill wraps the v3 CRM API with the six go-to-market roles each business actually has, and gives every one of them an AI partner with its own tool set.
HubSpot started as an inbound marketing tool and grew, hub by hub, into a full customer platform. Underneath it's a single CRM graph โ contacts, companies, deals, tickets, line items โ with five product surfaces layered on top. You integrate with the graph once, and every hub works.
| Hub | What it owns | Key CRM objects |
|---|---|---|
| Marketing | Campaigns, landing pages, email, forms, ads, lists | contacts, lists, campaigns, forms |
| Sales | Pipelines, deals, quotes, sequences, meetings | deals, contacts, companies, line items, quotes |
| Service | Tickets, SLAs, knowledge base, feedback | tickets, contacts, companies, conversations |
| CMS | Website, blog, HubDB, smart content | pages, HubDB rows, contacts |
| Operations | Data sync, workflows, custom code, data quality | properties, pipelines, workflows, all objects |
The 2nth model: one GTM role + one AI. HubSpot is already multi-human by design โ sales reps, marketers, CS agents, and RevOps engineers all work in the same portal on the same records. The canonical 2nth skill formalises six roles and gives each one its own AI partner with a narrowly scoped tool set. The human decides; the AI enables โ never the other way around.
| Role | The human decides | The AI enables |
|---|---|---|
| Revenue Leader | Forecast, territory, quota, GTM strategy | Pipeline health, win-rate drift, stalled-deal triage, forecast scoring |
| Sales Rep | Deal relationships, call priorities, closing moves | Next-best action, CRM hygiene, draft follow-ups, meeting summaries |
| Marketing Manager | Campaign strategy, positioning, budget allocation | Segment lists, subject-line A/B, attribution reports, asset performance |
| Service / CS Lead | Escalations, voice of customer, NPS | Ticket routing, sentiment flags, draft replies, SLA tracking |
| RevOps Engineer | Object model, process design, pipeline stages | Property inventories, workflow audits, data-quality reports, association mapping |
| Executive / Owner | GTM priorities, resource allocation, board narrative | Revenue dashboards, campaign ROI, churn signals, cross-hub rollups |
Don't try to replace HubSpot's internals. Read contacts, deals, tickets, and campaigns, enrich them with AI, and write back approved changes as first-class CRM mutations. The pipeline, workflow engine, and email sending infrastructure all stay where they are. Your job is the reasoning that sits on top.
HubSpot has three auth flavours. For integrations inside a single portal โ which is what almost every 2nth deployment is โ you want a Private App. You'll never touch OAuth unless you're shipping a public marketplace app. API keys are deprecated and should never be used in new work.
| Auth flavour | Use when | Header |
|---|---|---|
| Private App token | Internal integration, single portal. Scopes defined at creation time in the HubSpot UI. | Authorization: Bearer pat-na1-... |
| OAuth 2.0 | Public marketplace app, multi-portal. Install flow + refresh tokens. | Authorization: Bearer {access_token} |
| API key (legacy) | Do not use. Deprecated in 2022, read-only grandfathered support only. | ?hapikey=... (legacy) |
# Every v3 CRM request takes the same header Authorization: Bearer pat-na1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx Content-Type: application/json # v3 CRM API base https://api.hubapi.com/crm/v3/objects/{objectType} https://api.hubapi.com/crm/v3/objects/{objectType}/search https://api.hubapi.com/crm/v4/associations/{fromObjectType}/{toObjectType}/batch/read
// Cloudflare Worker โ HubSpot contact lookup by email const res = await fetch( 'https://api.hubapi.com/crm/v3/objects/contacts/search', { method: 'POST', headers: { 'Authorization': `Bearer ${env.HUBSPOT_PRIVATE_APP_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ filterGroups: [{ filters: [{ propertyName: 'email', operator: 'EQ', value: '[email protected]' }] }], properties: ['email', 'firstname', 'lastname', 'lifecyclestage'], limit: 1, }), } ); const { results } = await res.json();
Private App tokens are scope-gated at creation. A token with crm.objects.contacts.read can't touch deals, and a token with tickets can't read line items. Create one Private App per AI role rather than one omni-token โ a leaked marketer token should not be able to delete a deal. Store tokens in wrangler secret, rotate quarterly, and never expose them to the browser.
Four patterns carry almost every HubSpot integration: the CRM object endpoints for straight CRUD, Associations v4 for the graph edges, the Search API for anything that needs filters, and webhooks for reactive flows. Learn these four and 90% of HubSpot work becomes shape-fitting.
CRM objects โ the endpoints you'll touch most. Every object in HubSpot โ standard or custom โ follows the same URL shape. Swap contacts for companies, deals, tickets, or a custom object name and the rest is identical.
# Read โ list and by ID GET /crm/v3/objects/contacts?limit=100&properties=email,firstname,lastname GET /crm/v3/objects/contacts/{id}?properties=email,lifecyclestage # Write โ single and batch POST /crm/v3/objects/contacts PATCH /crm/v3/objects/contacts/{id} POST /crm/v3/objects/contacts/batch/create POST /crm/v3/objects/contacts/batch/update # Same shape for deals, tickets, companies, line items, quotes, custom objects GET /crm/v3/objects/deals?properties=dealname,amount,dealstage,pipeline GET /crm/v3/objects/tickets?properties=subject,hs_ticket_priority,hs_pipeline_stage GET /crm/v3/objects/companies?properties=name,domain,industry
Associations v4 โ the graph lives in the edges. A contact without its company is half a record; a deal without its contacts is useless. Associations v4 is the good API for this โ labelled, typed, batchable. Don't use v3 in new work.
# Read associations for a single contact GET /crm/v4/objects/contacts/{contactId}/associations/companies GET /crm/v4/objects/contacts/{contactId}/associations/deals # Batch read โ associations for many objects at once (fast) POST /crm/v4/associations/contacts/deals/batch/read { "inputs": [{ "id": "101" }, { "id": "102" }, { "id": "103" }] } # Create a labelled association (custom types live in v4) PUT /crm/v4/objects/contacts/{contactId}/associations/deals/{dealId} [{ "associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 3 }]
Search API โ filterGroups are your friend. List endpoints are for simple paging. The moment you need to filter, sort, or combine conditions, you want /search. filterGroups are ORed together; filters inside a group are ANDed. The result cap is 10,000 per query โ paginate with after, never trust offset past 10k.
POST /crm/v3/objects/deals/search
{
"filterGroups": [{
"filters": [
{ "propertyName": "dealstage", "operator": "EQ", "value": "presentationscheduled" },
{ "propertyName": "amount", "operator": "GT", "value": "50000" },
{ "propertyName": "hs_lastmodifieddate", "operator": "GT", "value": "1700000000000" }
]
}],
"sorts": [{ "propertyName": "amount", "direction": "DESCENDING" }],
"properties": ["dealname", "amount", "dealstage", "hubspot_owner_id"],
"limit": 100,
"after": "0"
}
MCP tool matrix โ one GTM role, one tool set. Mapping the role model to actual MCP tools. Each role gets a bounded tool set and nothing more. The reader-heavy shape below is deliberate โ most HubSpot AI value is in diagnosis and drafting, not autonomous writes. Humans still approve the mutations that touch pipeline and money.
const ROLE_TOOLS = { revenue_leader: ['get_pipeline_health', 'get_forecast_rollup', 'get_win_rate_trend', 'list_stalled_deals'], sales_rep: ['get_my_deals', 'get_contact', 'draft_followup_email', 'log_engagement', 'get_next_best_action'], marketing: ['get_campaign_analytics', 'get_campaign_asset_metrics', 'search_contacts', 'build_segment', 'draft_subject_lines'], service: ['search_tickets', 'get_ticket', 'get_contact_history', 'draft_reply', 'flag_sentiment'], revops: ['list_properties', 'search_properties', 'audit_workflows', 'map_associations', 'get_data_quality_report'], executive: ['get_revenue_dashboard', 'get_campaign_roi', 'get_customer_growth', 'get_cross_hub_rollup'], };
The 2nth.ai environment exposes an official HubSpot MCP surface that maps cleanly to this role model. Current tools: get_campaign_analytics, get_campaign_asset_metrics, get_campaign_asset_types, get_campaign_contacts_by_type, get_crm_objects, search_crm_objects, get_properties, search_properties, search_owners, get_organization_details, get_user_details, tool_guidance. An agent loading this skill node pairs those tools with the role it's serving โ a marketing agent gets the campaign tools, a RevOps agent gets the property and owner tools.
Webhooks โ the reactive layer. HubSpot fires webhooks from a Public App (not a Private App โ one of the sharp edges). Subscribe to property changes and object creations, verify the X-HubSpot-Signature-v3 header, route to the right role's agent. Retries happen over 24 hours with exponential backoff, so idempotency on the receiver matters.
| Subscription | Use case |
|---|---|
contact.creation | Welcome sequence, segment assignment, lead scoring |
contact.propertyChange | Re-score on lifecyclestage or persona change |
deal.propertyChange | Alert on stage advance, stalled-deal detection |
ticket.creation | Sentiment flag, SLA clock start, CS triage |
company.propertyChange | Territory reassignment, account-based marketing signals |
Rate limits โ per portal, not per token. The daily and burst caps apply to the entire HubSpot account, so every Private App and OAuth token shares the same budget. Plan for this: one noisy batch job can starve the agents behind it.
| Plan | Burst | Daily |
|---|---|---|
| Free / Starter | 100 req / 10 sec | 250,000 |
| Professional | 100 req / 10 sec | 500,000 |
| Enterprise | 150 req / 10 sec | 1,000,000+ |
Six specific failure modes โ the ones that actually cost time the first time they happen.
The label in the UI is "Annual Revenue". The property name in the API is annualrevenue. If you filter on the label the response is silent โ empty results, no error. Always call GET /crm/v3/properties/{objectType} first to discover the internal names.
Paginate with after, and if your query might exceed 10k, split it by a time window โ hs_lastmodifieddate > T. Offset-based paging past 10,000 is silently truncated. Bulk backfills should use incremental windows, not one giant query.
All date properties โ createdate, hs_lastmodifieddate, custom datetimes โ are Unix milliseconds in GMT, not seconds, not ISO. Filtering with an ISO string returns empty results. Convert on the way in.
Webhooks only fire from Public Apps. If your integration is a Private App โ as most internal 2nth deployments are โ you need a minimal Public App alongside it just for the webhook subscription. This is a frequent surprise the first time someone needs reactive flows.
Creating ten Private Apps doesn't give you ten times the budget โ every token draws from the same burst and daily caps. Monitor X-HubSpot-RateLimit-Remaining headers across all tokens, not just the one you're currently using.
v3 still works but it pretends all associations are identical. v4 introduces labelled types โ primary, billing contact, influencer, decision-maker โ which is what you actually need for any real RevOps work. Build new integrations on v4 and never look back.
HubSpot sits in the middle of the GTM layer โ it reads from ERP for the real order history, feeds fin/reporting with pipeline and revenue numbers, and shares the six-role model with Shopify where the same customer may exist on both sides.
improves biz/crm โ the role-based AI pattern feeds back into the shared CRM skill alongside Salesforce and the rest.HubSpot's developer docs are good โ and the v3 / v4 split can be confusing. Always check which version an endpoint sits on before copying code; associations are v4, most objects are v3, and webhooks live under the Public App surface.