hubspot-api

$npx mdskill add anthropics/claude-tag-plugins/hubspot-api

Interact with HubSpot CRM records using the v3 API

  • Look up, create, update, or associate contacts, companies, deals, and tickets
  • Uses HubSpot's CRM API v3 and supports object types like contacts and custom objects
  • Triggers on keywords, URLs, or object IDs related to HubSpot records
  • Returns structured data or executes actions based on user intent

SKILL.md

.github/skills/hubspot-apiView on GitHub ↗
---
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.

More from anthropics/claude-tag-plugins

SkillDescription
asana-apiRead and manage Asana tasks, projects, sections, comments, and workspaces. Use this whenever the user wants to list or search tasks, create or update a task, complete a task, comment on a task, move tasks between projects or sections, look up a project or workspace, or ask "what's on my Asana list" — even if they don't say "API". Also use it for any app.asana.com URL or an Asana task/project gid. Always start from this skill when interacting with this service — its bundled scripts and recipes are the fastest path.
bigquery-apiRun SQL against Google BigQuery and browse its catalog — submit queries (sync or async), poll job status, page through results, list datasets/tables, and read table schemas. Use this whenever the user wants to query a BigQuery table, ask "what's in this dataset", check a BigQuery job's status, or mentions bigquery.googleapis.com or a `project.dataset.table` path. Always start from this skill when interacting with this service — its bundled scripts and recipes are the fastest path.
config-guideReference guide for configuring @Claude agents — agents, agent scopes, identity profiles, presets, connections, rules, GitHub repositories, and custom instructions. Explains the inheritance model and configuration best practices.
confluence-apiRead, search, and manage Confluence Cloud pages, spaces, blog posts, comments, attachments, and labels. Use this whenever the user wants to find a page, read a doc, search the wiki with CQL, create or update a page, add a comment, list pages in a space, pull an attachment, or ask "what does the wiki say about X" — even if they don't say "API". Also use it for any *.atlassian.net/wiki URL, or a CQL string when the context is wiki content rather than tickets. Always start from this skill when interacting with this service — its bundled scripts and recipes are the fastest path.
datadog-apiQuery and manage Datadog monitoring data — logs, metrics, monitors, dashboards, events, SLOs, traces, and incidents. Use this whenever the user wants to search logs, look at a metric, check which monitors are alerting, investigate a trace, pull SLO status, mute an alert, or ask "what's happening in Datadog" — even if they don't say "API". Also use it for any URL under *.datadoghq.com. Always start from this skill when interacting with this service — its bundled scripts and recipes are the fastest path.
debug-pluginsDiagnose why a plugin or skill configured in @Claude admin settings isn't loading. Checks mount directories, the Claude Code launch command, and startup logs from inside the running container, then explains what failed and how to fix it.
enterprise-searchSearch the company's enterprise knowledge index. Use this FIRST when starting any task that touches company-specific context - projects, people, policies, internal docs, prior decisions - before searching individual sources like Drive, Slack, or Jira directly. Also use it when the user asks "do we have a doc about X", "what's our policy on Y", or references internal initiatives by name. Always start from this skill when interacting with this service — its bundled scripts and recipes are the fastest path.
google-drive-apiSearch, read, create, update, export, and share files in Google Drive. Use this whenever the user wants to find a file in Drive, read a Google Doc or Sheet, upload a file, move something into a folder, change sharing permissions, or asks "what's in my Drive" — even if they don't say "API". Also use it for any URL under drive.google.com or docs.google.com, or a mention of a Drive file ID. Always start from this skill when interacting with this service — its bundled scripts and recipes are the fastest path.
grafana-apiWork with a Grafana instance — search and read dashboards, run datasource queries (Prometheus, Loki, PostgreSQL, etc.), inspect alert rules and silences, post annotations, and manage folders. Use this whenever the user mentions a Grafana dashboard, panel, or alert; pastes a Grafana URL; asks "what does this dashboard show", "query this metric in Grafana", "is this alert firing", "silence this alert", or wants to create/export a dashboard — even if they don't say "API". Always start from this skill when interacting with this service — its bundled scripts and recipes are the fastest path.
graphingCompose polished charts (timeseries, bar, line, area, pie, scatter, or anything else the data calls for) from tabular data using the chartkit primitives, producing PNG, SVG, or self-contained interactive HTML. Use when the user asks to chart, graph, plot, or visualize data and wants something better than raw matplotlib defaults.