Работа с HubSpot CRM через API
Находит, создаёт, обновляет и связывает контакты, компании, сделки и тикеты в HubSpot запросами к API.
- Что делает
- Находит, создаёт, обновляет и связывает контакты, компании, сделки и тикеты в HubSpot запросами к API.
- Когда брать
- Когда нужно найти контакт, создать сделку, обновить компанию, поискать по CRM или связать две записи в HubSpot.
- Когда не брать
- Если HubSpot не подключён к рабочему пространству: при постоянных ошибках 401 и 403 скилл только сообщает о проблеме.
- Пример запроса
- Найди в HubSpot все лиды, созданные с начала года, и создай сделку для компании «Ромашка».
- Нужно подключить
- терминал, доступ к HubSpot
Входит в плагин hubspot. В Cowork и Claude Code можно поставить плагин целиком.
Как включить
- Скачайте архив и распакуйте его.
- Положите папку
hubspot-apiв~/.claude/skills/. - Откройте Claude Code и опишите задачу своими словами: Claude подхватит скилл по описанию.
Текст
---
name: hubspot-api
description: Читай, создавай, обновляй, ищи и связывай записи HubSpot CRM — контакты, компании, сделки, тикеты и пользовательские объекты. Используй этот скилл всякий раз, когда пользователь хочет найти контакт, создать сделку, обновить компанию, поискать по CRM, связать две записи или спрашивает «что у нас в HubSpot», даже если слова «API» он не говорит. Также применяй его для любой ссылки на app.hubspot.com и при упоминании объекта или ID записи HubSpot. Всегда начинай с этого скилла, когда работаешь с этим сервисом: его готовые скрипты и рецепты — самый быстрый путь.
---
CRM API HubSpot (v3) устроен одинаково для всех типов объектов: каждый объект живёт по адресу https://api.hubapi.com/crm/v3/objects/{objectType} и поддерживает одни и те же базовые операции, поиск, пакетные операции и связи.
(HubSpot переходит на пути с версией по дате — /crm/objects/2026-03/... — как на преемника нынешней схемы. Пути v3/v4 ниже по-прежнему работают; HubSpot объявил, что v4 перестанет поддерживаться 2027-03-30, а дата конца поддержки v3 пока не назначена. Пока переходить не нужно.)
Ключевые понятия:
- Типы объектов задаются именем (
contacts,companies,deals,tickets) или числовымobjectTypeId(0-1,0-2,0-3,0-5). Пользовательские объекты —2-<n>или их псевдонимp_<name>. В адресе подходит любая из форм. - Свойства при чтении запрашиваются отдельно. Вызовы списка и получения возвращают лишь несколько свойств по умолчанию (
hs_object_id,createdate,lastmodifieddateи свои для каждого типа, напримерemail/firstnameу контактов). Всегда передавайproperties=со списком нужных полей через запятую, иначе записи будут выглядеть пустыми. - У каждого типа объекта есть своё основное отображаемое свойство и ключ для поиска дублей. Контакты проверяются по
email; компании — поdomain; у сделок и тикетов проверки дублей нет. Узнай ключ до создания записи. - Связи (associations) имеют типы. У распространённых (
contact_to_company,deal_to_contactи т. д.) есть встроенные ID типов. Можно также завести собственные подписи связей.
Настройка запросов
Аутентификацию обеспечивает среда выполнения — учётные данные подставляются в исходящие запросы к этому API, так что настраивать ничего не нужно. Не пытайся создавать, выпускать, обновлять или проверять токены и ключи. Переменные с учётными данными нужны только для того, чтобы запросы были составлены правильно; если какая-то из них не задана, подставь любое значение-заглушку. Постоянная ошибка 401/403 означает, что для этого рабочего пространства учётные данные не настроены — сообщи об этом, а не разбирайся с авторизацией.
Каждый запрос несёт заголовок с токеном (bearer). У настроенных учётных данных есть набор прав доступа (scopes) (crm.objects.contacts.read, crm.objects.deals.write и т. д.), которые определяют, к чему можно обращаться.
export HUBSPOT_ACCESS_TOKEN="placeholder" # injected by the runtime; any value works
Проверка подключения — убедись, что рабочее пространство подключено:
curl -sS "https://api.hubapi.com/crm/v3/objects/contacts?limit=1" \
-H "Authorization: Bearer ${HUBSPOT_ACCESS_TOKEN}" | jq .
Ошибка 403 означает, что запрос прошёл аутентификацию, но у настроенных учётных данных нет права на то, что ты пытался сделать.
Для краткости в рецептах ниже используется вспомогательная функция. Определи её один раз или добавляй флаг -H к каждому curl:
hsapi() { curl -sS "$@" -H "Authorization: Bearer ${HUBSPOT_ACCESS_TOKEN}" -H "Content-Type: application/json"; }
Основные операции
В операциях ниже показаны contacts, но те же пути работают для companies, deals, tickets и любого пользовательского объекта — просто замени сегмент {objectType}.
1. Список записей
Постраничный вывод по курсору, с отбором свойств, без поиска.
hsapi -G "https://api.hubapi.com/crm/v3/objects/contacts" \
--data-urlencode "limit=50" \
--data-urlencode "properties=email,firstname,lastname,lifecyclestage,hubspot_owner_id" \
--data-urlencode "archived=false" | jq '.results[] | {id, props: .properties}'
При ошибке ответ выглядит как {"status":"error", "message":..., "category":...}, поля .results в нём нет — см. раздел «Обработка ошибок». Если пишешь скрипт, добавь одну проверку (jq 'if .results then … else . end'); в рецептах ниже для наглядности показана простая выборка без проверки.
Добавь associations=companies,deals, чтобы получить вместе с каждой записью ID связанных объектов.
2. Получить одну запись
hsapi -G "https://api.hubapi.com/crm/v3/objects/contacts/CONTACT_ID" \
--data-urlencode "properties=email,firstname,lastname,phone,company" \
--data-urlencode "associations=companies,deals" | jq .
Чтобы получить запись по уникальному свойству, а не по внутреннему ID (например, по email), добавь idProperty=email и поставь email в путь:
hsapi -G "https://api.hubapi.com/crm/v3/objects/contacts/alice@example.com" \
--data-urlencode "idProperty=email" \
--data-urlencode "properties=email,firstname,lastname" | jq .
3. Создать запись
hsapi -X POST "https://api.hubapi.com/crm/v3/objects/contacts" \
-d '{
"properties": {
"email": "alice@example.com",
"firstname": "Alice",
"lastname": "Nguyen",
"lifecyclestage": "lead"
}
}' | jq 'if .status == "error" then . else {id, createdAt} end'
Чтобы создать и сразу связать записи одним вызовом, добавь массив associations:
hsapi -X POST "https://api.hubapi.com/crm/v3/objects/deals" \
-d '{
"properties": {
"dealname": "Acme expansion",
"pipeline": "default",
"dealstage": "appointmentscheduled",
"amount": "15000"
},
"associations": [
{"to": {"id": "COMPANY_ID"}, "types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 341}]}
]
}' | jq .
341 — это deal_to_company. См. таблицу типов связей в references/api.md или узнай их на лету (операция 8).
4. Обновить запись
Через PATCH отправляй только те свойства, которые хочешь изменить. Всё остальное остаётся нетронутым.
hsapi -X PATCH "https://api.hubapi.com/crm/v3/objects/contacts/CONTACT_ID" \
-d '{"properties": {"lifecyclestage": "marketingqualifiedlead", "phone": "+1-555-0100"}}'
Обновление по уникальному свойству работает так же, как чтение: добавь ?idProperty=email и поставь email в путь.
5. Архивировать / удалить запись
«Удаление» в HubSpot мягкое (запись попадает в корзину примерно на 90 дней). Успех — это 204 с пустым телом, поэтому выводи код статуса:
hsapi -X DELETE "https://api.hubapi.com/crm/v3/objects/contacts/CONTACT_ID" -w '\n%{http_code}\n'
Пакетное удаление: POST /crm/v3/objects/contacts/batch/archive с {"inputs": [{"id": "..."}, ...]}.
6. Поиск записей (scripts/hs_search.sh)
Ищи по любому типу объектов через входящий в комплект скрипт (путь указан относительно папки этого скилла): он собирает тело запроса из filterGroups/sorts/query, отправляет его на /crm/v3/objects/{type}/search, проходит по paging.next.after через все страницы и выдаёт TSV или JSONL.
scripts/hs_search.sh --object contacts \
--filter lifecyclestage:EQ:lead \
--filter createdate:GTE:2024-01-01T00:00:00Z \
--sort createdate:desc --limit 200
--object TYPE(обязательный) — этоcontacts,companies,deals,ticketsили любое имя типа объекта /objectTypeId. Параметры подключения берутся изHUBSPOT_ACCESS_TOKEN, см. выше.--filter PROP:OP:VALUE(можно повторять) объединяет фильтры по «И» в однойfilterGroup.OPпишется заглавными буквами;IN/NOT_INпринимают список через запятую (dealstage:IN:won,lost→values; для строковых свойств значения должны быть в нижнем регистре),BETWEENпринимаетlow,high(amount:BETWEEN:100,500→value+highValue),HAS_PROPERTY/NOT_HAS_PROPERTYзначения не принимают.--query TEXTдобавляет поиск по фразе в свободном тексте.--properties LISTзадаёт и запрос, и столбцы TSV; по умолчанию они свои для каждого типа (email,firstname,lastname,createdateдля контактов и т. д.), а для любого другого типа указывать их обязательно.--sort PROP[:desc]сортирует результаты.--limit Nограничивает число получаемых строк (по умолчанию 100,0= все — поиск жёстко ограничен 10 000 результатами);--page-size N(максимум 200);--jsonвыдаёт по одному JSON-объекту на запись вместо TSV с заголовком. Общее число найденных записей и число строк выводятся в stderr.- Коды завершения:
0— успех,1— запрос не удался, ошибка API (categoryиmessageв stderr) или неверные аргументы.
Если скрипт выдаёт ошибку, прочитай его: это обычные curl + jq, — и разбирайся по references/api.md.
Скрипт собирает одну filterGroup с условиями по «И». Для групп по «ИЛИ» отправь готовое тело через hsapi — лимиты: 5 групп, 6 фильтров в группе, 18 фильтров всего. Поиск обновляется с задержкой (eventually consistent): запись, созданная или изменённая несколько секунд назад, может ещё не появиться — для чтения сразу после записи используй прямой GET по ID.
7. Пакетное чтение / создание / обновление / upsert
Пакетные эндпоинты batch/* заменяют много обращений одним — до 100 записей (у некоторых лимит ниже — см. references/api.md). Все пять глаголов используют один и тот же формат {"inputs": [...]}.
# batch read by id
hsapi -X POST "https://api.hubapi.com/crm/v3/objects/contacts/batch/read" \
-d '{"inputs": [{"id": "101"}, {"id": "102"}], "properties": ["email", "firstname"]}'
# upsert by a unique property (create if new, update if the key matches)
hsapi -X POST "https://api.hubapi.com/crm/v3/objects/contacts/batch/upsert" \
-d '{"inputs": [{"idProperty": "email", "id": "a@x.com", "properties": {"firstname": "Alice"}}]}'
batch/create и batch/update принимают соответственно {"inputs": [{"properties": {...}}]} и {"inputs": [{"id": "...", "properties": {...}}]}.
8. Список и создание связей
Используй эндпоинты связей v4 — они новее и несут информацию о подписях и типах, которая тебе действительно нужна. Они работают параллельно с v3.
# what's associated with this contact?
hsapi "https://api.hubapi.com/crm/v4/objects/contacts/CONTACT_ID/associations/companies" | jq '.results'
# discover association type ids between two object types
hsapi "https://api.hubapi.com/crm/v4/associations/contacts/companies/labels" | jq '.results'
# create an association (PUT — idempotent)
hsapi -X PUT "https://api.hubapi.com/crm/v4/objects/contacts/CONTACT_ID/associations/companies/COMPANY_ID" \
-d '[{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 1}]'
# remove (204 on success, empty body)
hsapi -X DELETE "https://api.hubapi.com/crm/v4/objects/contacts/CONTACT_ID/associations/companies/COMPANY_ID" -w '\n%{http_code}\n'
9. Узнать свойства (схему)
Прочитай каталог свойств типа объекта, чтобы узнать названия полей, типы и options для полей с перечислениями (воронки, этапы, стадии жизненного цикла и т. д.). Делай это до создания записей или фильтрации — угадывание названий свойств остаётся самой частой причиной ошибок 400.
hsapi "https://api.hubapi.com/crm/v3/properties/contacts" | \
jq '.results[] | {name, label, type, fieldType, options: (.options | length)}'
# one property with its option values
hsapi "https://api.hubapi.com/crm/v3/properties/deals/dealstage" | jq '{name, options: [.options[] | {label, value}]}'
10. Воронки и этапы
Сделки и тикеты живут в воронках (pipelines) с упорядоченными этапами. В свойства dealstage / hs_pipeline_stage подставляются ID этапов и воронок, а не подписи.
hsapi "https://api.hubapi.com/crm/v3/pipelines/deals" | \
jq '.results[] | {id, label, stages: [.stages[] | {id, label, displayOrder}]}'
Постраничная выдача
Эндпоинты списков (GET /crm/v3/objects/{type}, списки свойств, списки связей) и поиск (POST .../search) используют курсорную пагинацию: если результатов больше, в ответе есть paging.next.after; передай его обратно как after (параметр запроса для GET, поле тела для поиска). limit не больше 100 для списка и 200 для поиска; кроме того, поиск жёстко ограничен 10 000 результатов в сумме на один запрос — если нужно больше, сузь фильтр. В теле ошибки нет .paging — ограничивай любой цикл и прерывай его при .status == "error".
Лимиты запросов
Токены приватных приложений получают 100 запросов за 10 секунд на аккаунтах Free/Starter и 190 за 10 секунд на Professional/Enterprise (плюс суточные лимиты, растущие с уровнем аккаунта). Эндпоинты поиска ограничены 5 запросами в секунду на аккаунт, и в их ответах нет заголовков с лимитами. Все остальные ответы содержат:
X-HubSpot-RateLimit-Max сколько запросов разрешено за интервал
X-HubSpot-RateLimit-Remaining сколько запросов осталось в текущем интервале
X-HubSpot-RateLimit-Interval-Milliseconds длина интервала
X-HubSpot-RateLimit-Daily / -Daily-Remaining
При 429 подожди столько, сколько указано в X-HubSpot-RateLimit-Interval-Milliseconds (или около 10 с, если заголовка нет), и повтори запрос. Предпочитай пакетные эндпоинты — один пакетный вызов на 100 записей считается за один запрос.
Обработка ошибок
Ошибки приходят в JSON: {"status": "error", "message": "...", "correlationId": "...", "category": "..."}. Проверяй .status, прежде чем выбирать .results / .id — в ответе с ошибкой их нет, и простая выборка выведет null. Всегда показывай message и category.
- **
400VALIDATION_ERROR** — неверное имя или значение свойства, некорректный фильтр, неправильное значение перечисления. Прочитайerrors[]— там указано проблемное поле. Сверься с каталогом свойств (операция 9). - **
401INVALID_AUTHENTICATION** — учётные данные отсутствуют или отклонены. Проверь, чтоHUBSPOT_ACCESS_TOKENвообще задан; если ошибка не уходит, значит, для этого рабочего пространства учётные данные не настроены — сообщи об этом. - **
403MISSING_SCOPES** — у настроенных учётных данных нет права на этот эндпоинт. В сообщении названо недостающее право — сообщи о нём. - **
404OBJECT_NOT_FOUND** — неверный ID, неправильныйobjectTypeили запись удалена/архивирована. Попробуйarchived=true. - **
409CONFLICT** — дубль по уникальному ключу (например, создание контакта с уже существующим email). Переключись на upsert или обнови существующую запись через patch. - **
429RATE_LIMITS** — полеpolicyNameв теле ответа говорит, какой лимит достигнут: посекундный/всплесковый — подожди, сколько указано вX-HubSpot-RateLimit-Interval-Milliseconds, и повтори;DAILY— остановись, лимит сбросится в полночь по времени аккаунта. - **
5xx** — временная ошибка. Повтори с нарастающей паузой.
Подробнее
В references/api.md — более полный каталог эндпоинтов: полные списки свойств по умолчанию для каждого типа объекта, таблица встроенных ID типов связей, лимиты пакетных операций, эндпоинты управления владельцами, воронками и свойствами, схемы пользовательских объектов, списки и типы объектов-взаимодействий (заметки, звонки, задачи, встречи, письма). Читай его, когда нужен эндпоинт или ID, которых нет выше.
Перевод: iiuniversitet. Оригинал: https://github.com/anthropics/claude-tag-plugins/tree/main/hubspot/skills/hubspot-api, лицензия Apache-2.0. Изменения: перевод на русский язык.
Оригинал на английском
---
name: hubspot-api
description: Read, create, update, search, and associate HubSpot CRM records — contacts, companies, deals, tickets, and custom objects. Use this whenever the user wants to look up a contact, create a deal, update a company, search the CRM, link two records, or asks "what's in HubSpot" — even if they don't say "API". Also use it for any URL under app.hubspot.com or a mention of a HubSpot object/record ID. Always start from this skill when interacting with this service — its bundled scripts and recipes are the fastest path.
---
HubSpot's CRM API (v3) is uniform across object types: every object lives under `https://api.hubapi.com/crm/v3/objects/{objectType}` and supports the same basic/search/batch/association operations.
(HubSpot is rolling out date-based path versions — `/crm/objects/2026-03/...` — as the successor naming. The `v3`/`v4` paths below all still work; HubSpot has announced `v4` becomes unsupported on 2027-03-30 and `v3`'s end-of-support date is not yet set. No migration is required for now.)
**Key concepts:**
- **Object types** are identified by name (`contacts`, `companies`, `deals`, `tickets`) or by numeric
`objectTypeId` (`0-1`, `0-2`, `0-3`, `0-5`). Custom objects are `2-<n>` or their `p_<name>` alias.
Either form works in the URL.
- **Properties are opt-in on reads.** List/get calls return only a handful of default properties
(`hs_object_id`, `createdate`, `lastmodifieddate`, and per-type defaults like `email` / `firstname`
for contacts). Always pass `properties=` with a comma-separated list of the fields you actually
need, or you'll get records that look empty.
- **Each object type has its own primary display property and dedup key.** Contacts dedup on `email`;
companies on `domain`; deals and tickets have no dedup. Know the key before creating.
- **Associations are typed.** The common ones (`contact_to_company`, `deal_to_contact`, etc.) have
built-in type IDs. You can also define custom association labels.
## Request setup
Authentication is handled by the runtime — credentials are injected into outbound requests to this
API, so there is nothing to set up. Do not try to create, mint, refresh, or validate tokens or keys.
Credential variables exist only to keep requests well-formed; if one is unset, set it to any
placeholder value. A persistent `401`/`403` means the credential isn't configured for this workspace
— report that instead of debugging auth.
Every request carries a bearer header. The configured credential has a set of scopes
(`crm.objects.contacts.read`, `crm.objects.deals.write`, etc.) that control what it can touch.
```bash
export HUBSPOT_ACCESS_TOKEN="placeholder" # injected by the runtime; any value works
```
**Sanity check** — confirm the workspace is wired up:
```bash
curl -sS "https://api.hubapi.com/crm/v3/objects/contacts?limit=1" \
-H "Authorization: Bearer ${HUBSPOT_ACCESS_TOKEN}" | jq .
```
A `403` means the request authenticated but the configured credential lacks the scope for what you
tried.
For brevity the recipes below use a helper. Define it once, or copy the `-H` flag onto each `curl`:
```bash
hsapi() { curl -sS "$@" -H "Authorization: Bearer ${HUBSPOT_ACCESS_TOKEN}" -H "Content-Type: application/json"; }
```
## Core operations
The operations below use `contacts`, but the same paths work for `companies`, `deals`, `tickets`,
and any custom object — just swap the `{objectType}` segment.
### 1. List records
Cursor-paginated, property-filtered, no search.
```bash
hsapi -G "https://api.hubapi.com/crm/v3/objects/contacts" \
--data-urlencode "limit=50" \
--data-urlencode "properties=email,firstname,lastname,lifecyclestage,hubspot_owner_id" \
--data-urlencode "archived=false" | jq '.results[] | {id, props: .properties}'
```
On failure the response is `{"status":"error", "message":..., "category":...}` and has no `.results`
— see Error handling. Guard once if scripting (`jq 'if .results then … else . end'`); the recipes
below show the bare projection for clarity.
Add `associations=companies,deals` to inline association IDs for each record.
### 2. Get one record
```bash
hsapi -G "https://api.hubapi.com/crm/v3/objects/contacts/CONTACT_ID" \
--data-urlencode "properties=email,firstname,lastname,phone,company" \
--data-urlencode "associations=companies,deals" | jq .
```
To fetch by a unique property instead of internal ID (e.g. by email), add `idProperty=email` and put
the email in the path:
```bash
hsapi -G "https://api.hubapi.com/crm/v3/objects/contacts/alice@example.com" \
--data-urlencode "idProperty=email" \
--data-urlencode "properties=email,firstname,lastname" | jq .
```
### 3. Create a record
```bash
hsapi -X POST "https://api.hubapi.com/crm/v3/objects/contacts" \
-d '{
"properties": {
"email": "alice@example.com",
"firstname": "Alice",
"lastname": "Nguyen",
"lifecyclestage": "lead"
}
}' | jq 'if .status == "error" then . else {id, createdAt} end'
```
To create **and associate** in one call, add an `associations` array:
```bash
hsapi -X POST "https://api.hubapi.com/crm/v3/objects/deals" \
-d '{
"properties": {
"dealname": "Acme expansion",
"pipeline": "default",
"dealstage": "appointmentscheduled",
"amount": "15000"
},
"associations": [
{"to": {"id": "COMPANY_ID"}, "types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 341}]}
]
}' | jq .
```
`341` is `deal_to_company`. See the association-type table in `references/api.md`, or discover them
live (op 8).
### 4. Update a record
`PATCH` only the properties you want to change. Everything else is untouched.
```bash
hsapi -X PATCH "https://api.hubapi.com/crm/v3/objects/contacts/CONTACT_ID" \
-d '{"properties": {"lifecyclestage": "marketingqualifiedlead", "phone": "+1-555-0100"}}'
```
Updating by unique property works the same as reading — append `?idProperty=email` and put the email
in the path.
### 5. Archive / delete a record
HubSpot "deletes" are soft (the record goes to the recycle bin for ~90 days). Success is a `204`
with an empty body, so print the status code:
```bash
hsapi -X DELETE "https://api.hubapi.com/crm/v3/objects/contacts/CONTACT_ID" -w '\n%{http_code}\n'
```
Batch delete: `POST /crm/v3/objects/contacts/batch/archive` with `{"inputs": [{"id": "..."}, ...]}`.
### 6. Search records (`scripts/hs_search.sh`)
Search any object type through the bundled script (path is relative to this skill's directory): it
builds the `filterGroups`/`sorts`/`query` body, posts to `/crm/v3/objects/{type}/search`, follows
`paging.next.after` through every page, and emits TSV or JSONL.
```bash
scripts/hs_search.sh --object contacts \
--filter lifecyclestage:EQ:lead \
--filter createdate:GTE:2024-01-01T00:00:00Z \
--sort createdate:desc --limit 200
```
- `--object TYPE` (required) is `contacts`, `companies`, `deals`, `tickets`, or any object type
name / `objectTypeId`. Instance specifics come from `HUBSPOT_ACCESS_TOKEN` above.
- `--filter PROP:OP:VALUE` (repeatable) ANDs filters in one `filterGroup`. `OP` is uppercased;
`IN` / `NOT_IN` take a comma list (`dealstage:IN:won,lost` → `values`; for string properties
the values must be lowercase), `BETWEEN` takes `low,high` (`amount:BETWEEN:100,500` → `value` +
`highValue`), `HAS_PROPERTY` / `NOT_HAS_PROPERTY` take no value. `--query TEXT` adds a free-text
phrase match.
- `--properties LIST` drives both the request and the TSV columns; defaults are per-type
(`email,firstname,lastname,createdate` for contacts, etc.) and required for any other type.
`--sort PROP[:desc]` orders results.
- `--limit N` caps fetched rows (default 100, `0` = everything — search hard-caps at 10,000
results); `--page-size N` (max 200); `--json` emits one JSON object per record instead of TSV
with a header. Total match count and row counts go to stderr.
- Exit codes: `0` success, `1` request failed or API error (`category` and `message` on stderr) or
bad arguments.
If the script errors, read it — it's plain `curl` + `jq` — and debug against `references/api.md`.
The script emits one ANDed `filterGroup`. For ORed groups, post the raw body via `hsapi` — limits
are 5 groups, 6 filters per group, **18 filters total**. Search is eventually consistent: a record
created or updated seconds ago may not appear yet — use a direct `GET` by ID for read-after-write.
### 7. Batch read / create / update / upsert
The `batch/*` endpoints trade one round trip for up to 100 records (some are lower — see
`references/api.md`). All five verbs follow the same `{"inputs": [...]}` shape.
```bash
# batch read by id
hsapi -X POST "https://api.hubapi.com/crm/v3/objects/contacts/batch/read" \
-d '{"inputs": [{"id": "101"}, {"id": "102"}], "properties": ["email", "firstname"]}'
# upsert by a unique property (create if new, update if the key matches)
hsapi -X POST "https://api.hubapi.com/crm/v3/objects/contacts/batch/upsert" \
-d '{"inputs": [{"idProperty": "email", "id": "a@x.com", "properties": {"firstname": "Alice"}}]}'
```
`batch/create` and `batch/update` take `{"inputs": [{"properties": {...}}]}` and
`{"inputs": [{"id": "...", "properties": {...}}]}` respectively.
### 8. List and create associations
Use the v4 associations endpoints — they're newer and carry the label/type info you actually want.
They coexist with v3.
```bash
# what's associated with this contact?
hsapi "https://api.hubapi.com/crm/v4/objects/contacts/CONTACT_ID/associations/companies" | jq '.results'
# discover association type ids between two object types
hsapi "https://api.hubapi.com/crm/v4/associations/contacts/companies/labels" | jq '.results'
# create an association (PUT — idempotent)
hsapi -X PUT "https://api.hubapi.com/crm/v4/objects/contacts/CONTACT_ID/associations/companies/COMPANY_ID" \
-d '[{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 1}]'
# remove (204 on success, empty body)
hsapi -X DELETE "https://api.hubapi.com/crm/v4/objects/contacts/CONTACT_ID/associations/companies/COMPANY_ID" -w '\n%{http_code}\n'
```
### 9. Discover properties (schema)
Read the property catalog for an object type to learn field names, types, and `options` for
enumerated fields (pipelines, stages, lifecycle stages, etc.). Do this before creating or filtering
— guessing property names is the most common source of `400` errors.
```bash
hsapi "https://api.hubapi.com/crm/v3/properties/contacts" | \
jq '.results[] | {name, label, type, fieldType, options: (.options | length)}'
# one property with its option values
hsapi "https://api.hubapi.com/crm/v3/properties/deals/dealstage" | jq '{name, options: [.options[] | {label, value}]}'
```
### 10. Pipelines and stages
Deals and tickets live in pipelines with ordered stages. Stage and pipeline **IDs** (not labels) are
what go in `dealstage` / `hs_pipeline_stage` properties.
```bash
hsapi "https://api.hubapi.com/crm/v3/pipelines/deals" | \
jq '.results[] | {id, label, stages: [.stages[] | {id, label, displayOrder}]}'
```
## Pagination
List endpoints (`GET /crm/v3/objects/{type}`, property lists, association lists) and search
(`POST .../search`) both use cursor pagination: response carries `paging.next.after` when more
results exist; pass it back as `after` (query param for GET, body field for search). `limit` maxes
at 100 for list, 200 for search; search is additionally hard-capped at **10,000 total results** per
query — narrow the filter if you need more. An error body has no `.paging` — bound any loop and
break on `.status == "error"`.
## Rate limits
Private-app tokens get **100 requests per 10 seconds** on Free/Starter accounts and **190 per 10
seconds** on Professional/Enterprise (plus daily caps that scale with the account tier). Search
endpoints are limited to **5 requests per second per account** and their responses do not carry the
rate-limit headers. Every other response carries:
```
X-HubSpot-RateLimit-Max requests allowed per interval
X-HubSpot-RateLimit-Remaining requests left in the current interval
X-HubSpot-RateLimit-Interval-Milliseconds interval length
X-HubSpot-RateLimit-Daily / -Daily-Remaining
```
On `429`, sleep for `X-HubSpot-RateLimit-Interval-Milliseconds` (or ~10s if absent) and retry.
Prefer batch endpoints — one batch call of 100 records counts as one request.
## Error handling
Errors are JSON: `{"status": "error", "message": "...", "correlationId": "...", "category": "..."}`.
Check `.status` before projecting `.results` / `.id` — the error envelope has neither and a bare
projection prints nulls. Always surface `message` and `category`.
- **`400` `VALIDATION_ERROR`** — Bad property name/value, malformed filter, wrong enum value. Read `errors[]` — it names the offending field. Check the property catalog (op 9).
- **`401` `INVALID_AUTHENTICATION`** — Credential missing or rejected. Check `HUBSPOT_ACCESS_TOKEN` is set at all; if it persists, the credential isn't configured for this workspace — report it.
- **`403` `MISSING_SCOPES`** — The configured credential lacks the scope for this endpoint. The message names the missing scope — report it.
- **`404` `OBJECT_NOT_FOUND`** — Bad ID, wrong `objectType`, or record was deleted/archived. Try `archived=true`.
- **`409` `CONFLICT`** — Duplicate on a unique key (e.g., creating a contact with an existing email). Switch to upsert or patch the existing record.
- **`429` `RATE_LIMITS`** — The body's `policyName` says which limit you hit: a secondly/burst policy → sleep per `X-HubSpot-RateLimit-Interval-Milliseconds`, retry; `DAILY` → stop, it resets at midnight account-local time.
- **`5xx`** — Transient. Retry with backoff.
## Going deeper
`references/api.md` has the fuller endpoint catalog — the complete default-property lists per object
type, the built-in association type ID table, batch limits, the owners/pipelines/properties
management endpoints, custom object schemas, lists, and the engagement object types (notes, calls,
tasks, meetings, emails). Read it when you need an endpoint or ID not covered above.
Источник: anthropics/claude-tag-plugins / hubspot / hubspot-api ↗. Ссылка проверена 2026-10-10.