hubspot
hubspot.
HubSpot CRM objects — one tool, the verb in `op`.
setup & usage
when the instance publishes its hubspot apps, the card shows connect hubspot: you pick the portal, approve, and you are connected — no key to create, no scope to tick. pick the tab first: it decides which kind of connection you make.
no "connect hubspot" button on a tab: the instance has no hubspot app for that kind of connection yet — the service key below works the same.
hubspot authenticates via a service key. ⚠️ hubspot stops letting you create *private apps* (on 28/09/2026 for new accounts, 26/10/2026 for existing ones — changelog); a private app made before keeps working, and its token is pasted the same way.
in hubspot, open development → keys → service keys (on some accounts: settings → integrations → service keys), click create service key (super admin or developer tools access needed), name it and tick the scopes (names as hubspot's scope list spells them — each comes as .read and .write):
crm.objects.contacts.read + .write, crm.objects.companies.read + .write, crm.objects.deals.read + .write — the crm itself (deal pipelines and stages come with crm.objects.deals.read)crm.objects.tickets.read + .write — tickets. and tickets if your scope list still shows it: ticket *pipelines* and ticket *properties* still ask for that older scopecrm.schemas.contacts.read, crm.schemas.companies.read, crm.schemas.deals.read (+ crm.schemas.tickets.read if offered) — to discover an object's propertiescrm.lists.read + crm.lists.write — for lists (= segments)crm.objects.owners.read — to list ownersbusiness-intelligence — optional, only for the pages a contact read (hubspot_visits, op page_views). the connection test does not ask for it. ⚠️ it may not be enough: see *website visitors* belowthere is no crm.objects.tickets scope (without .read/.write): if a guide told you to tick it, tick crm.objects.tickets.read instead.
⚠️ if your key (or private app) predates the lists tools, it only has the crm.objects.* scopes and hubspot_list will answer 403. that does not mean your key is wrong: reopen it in hubspot and add the scope. adding a scope does not change a private app's token; if you rotate the key in hubspot, its new value replaces the old one: paste it again in oto (connector hubspot).
test the connection tells you more than "it authenticates": it reads, family by family, which scopes the token holds (connected, tickets scope missing). a missing family does not turn the connection red — the rest keeps working. it also names the portal the key reads (identity: portal id, region, account type).
⚠️ the expected token is the service key's value, or a private app's access token (both of the form pat-<region>-…). if you paste something else — typically an oauth *refresh token*, a long base64 blob starting with Ci… — hubspot answers 401 `EXPIRED_AUTHENTICATION` with a misleading message along the lines of "expired 20697 day(s) ago" and an expiry date of january 1st, 1970. nothing expired: a refresh token has no expiry field, so sent as a bearer it is read as expired since the epoch. that message means "this is not an access token", not "your token got old".
/account), connector hubspot### troubleshooting — 403 MISSING_SCOPES on one object type
contacts and deals answer, but tickets (or lists, or pipelines) answer 403 `MISSING_SCOPES`, with hubspot's message "the scope needed for this api call isn't available for public use". that message is misleading: the scope exists, it is just not ticked on your key. the refusal oto returns names the scope to add — tick it on the service key (or in the private app's scopes tab) and save. nothing to fix in the call.
### troubleshooting — 400 PROPERTY_DOESNT_EXIST on a write
a create or update names a property your portal does not have (e.g. linkedin_url on a company, where hubspot's own property is linkedin_company_page). hubspot refuses the whole call, nothing is written. the refusal oto returns names the unknown properties: read the real internal names with hubspot_property (op list) and fix the procedure, or create the property (op create) if it should exist. in hubspot_push_rows, the batch stops at the first row (stopped: hubspot_property_doesnt_exist, unknown_properties) instead of failing every row the same way.
query and edit your hubspot crm (contacts, companies, deals, tickets) from claude.
hubspot_object (op search, object_type contacts)hubspot_object (op create, object_type deals)hubspot_object (op associations)hubspot_object (op add_note)### merging duplicates — irreversible
hubspot_object (op merge, object_type companies, object_id = the record that survives, merge_ids = the duplicates)⚠️ a hubspot merge cannot be undone. always run it first with dry_run=true: it reads the survivor and each duplicate (name and domain, or email; how many contacts and deals each one carries) and tells you what would happen, without writing anything.
merged, already_merged (its id already leads to another record: hubspot keeps answering a merged id with its survivor), not_found, or error with hubspot's status and message — and one failure never stops the otherssurvivor_id in the answer, not the id you passed### segments = lists
in hubspot, a "segment" is a list: there is no separate segments api. hubspot_list covers them.
hubspot_list (op search, object_type contacts)create then op add_membersmembersrecord_liststhree list types, chosen at creation and not changeable afterwards:
add_members / remove_members)update, filter_branch)emptying a list (clear_members) and deleting it (delete) accept dry_run=true: it tells you what would go without touching anything. a deleted list stays restorable for 90 days (op restore).
### properties — read before writing
hubspot internal names are not the interface labels (dealstage, not "Deal Stage"), and a dropdown only accepts its declared values. hubspot_property (op list) gives the real schema of an object type — on a fresh portal, that is already 404 properties on contacts, 45 of them enumerations. this is what makes the create/update of hubspot_object reliable and the filter_branch criteria of a dynamic list, which reference properties by internal name.
op list returns one compact row per property (name, label, type, field type, group, options) and leaves out hubspot's hidden internal ones — include_hidden=true keeps them, verbose=true gives the full cards. a deal's stages belong to a *pipeline*, not to the dealstage property: their options are filled from the pipelines, grouped by pipeline.
hubspot_property (op list, object_type deals, custom_only=true)### pipelines and stages
hubspot_pipeline (op list, object_type deals) — stage ids, labels, order, win probability, closed or not; archived stages are kept and flaggedhubspot_object (op search, object_type deals, resolve_labels=true) — adds dealstage_label and pipeline_label next to the raw ids, which stay (a stage is written by its id)hs_pipeline_stage, ticketState open or closed)### website visitors
hubspot_visits reads who came to your site, latest first. three sources, and they do not measure the same thing:
companies — the companies hubspot identified from their network (buyer intent): last visit time, visitors and page views over 30 days, recent intent signalscontacts — people hubspot already knows (a form, an email click): last session, last page, first page, session count. anonymous visitors are not contacts and are not herepage_views with the contact's id — each page with its timewhat hubspot needs on its side, per its own documentation (we have not yet read a portal with tracking on):
companies: companies tracked in buyer intent. hubspot only writes the visit on the companies it tracks — in buyer intent, a saved view with add new companies and tracking intent signals turned on does it for every company entering the view from then on. tracking is paid in hubspot credits: 10 per company and per month, against the credits a plan includes (500 a month on starter, 3,000 on professional, 5,000 on enterprise) — hubspot's figures as of october 2026, check its catalogpage_views: hubspot's reference asks for the business-intelligence scope and a professional or enterprise plan. ⚠️ on a portal read in october 2026, a private app holding that scope was still refused — hubspot asked for event-detail-read, which is in no published scope list. until a private app is seen passing, treat a contact's page views as unavailable; the first page, last page and last session (op contacts) always workwhat hubspot does not expose: the pages a *company* read. the buyer intent card shows them in hubspot; the api only carries the two 30-day counts. on a company, page paths need a visitor-identification tool that writes them.
an empty op companies answer says why: its diagnosis counts the tracked companies — zero means tracking was never turned on, not that nobody came.
to run it on a schedule ("new visitors since the last run"), pass since = the last run's time minus a few minutes, and deduplicate on the record id: hubspot's search index lags a few moments behind a write. a segment works too — a list filtered on recent intent signals, read with hubspot_list (op members, with properties), whose rows carry the date each company entered it.
### searching: order and pages
hubspot_object (op search) takes sorts — one rule, e.g. [{"propertyName": "createdate", "direction": "DESCENDING"}]. a search serves at most 10,000 results: narrow the filters rather than paging furtherhubspot_list (op search) serves 20 lists a page: pass the answer's offset back while it says hasMore. processing_types keeps only MANUAL, DYNAMIC or SNAPSHOT liststhis connector accepts multiple comptes: each stored credential becomes a named account (one name per compte), at your level, your team's or your org's.
_account="<name>" on the tool; list them: oto_identity(op='list', connector='hubspot') (scope='org' or scope='group' for the org's or team's)oto_identity(op='set', connector='hubspot', identity_id='<name>'); rename: op='rename' with new_name_account) and adding upoutils
utilisation
claude plugin marketplace add otomata-tech/oto-plugin — mcp + skill configurés.pipx install oto-cli puis oto hubspot …