# HubSpot CLI

**JSON-first, agent-native CLI for the HubSpot CRM API.**

> HubSpot has an official CLI (`@hubspot/cli`) — but it only covers CMS developer tools (Design Manager, serverless functions, HubDB). There is **no official CLI for the CRM API**: contacts, companies, deals, tickets, pipelines, owners, engagements, or associations.
>
> This CLI fills that gap. Every command returns structured JSON, works from the terminal _and_ as an MCP tool — so your AI agents can manage your entire HubSpot CRM the same way a human would from the command line.

## Why Agent-Native?

| | `@hubspot/cli` (Official) | `hubspot-cli` (This) |
|---|---|---|
| **Scope** | CMS only (themes, modules, serverless) | Full CRM API — 55 commands across 11 groups |
| **Output** | Human-readable text | JSON-first — machine-parseable by default |
| **MCP Support** | No | Built-in MCP server — every command is an MCP tool |
| **Agent Use** | Not designed for agents | Every command works identically from terminal or MCP |
| **Auth** | OAuth / Personal Access Key | Private App access token (3-tier resolution) |

**Agent-native** means:
- **JSON-first output** — every response is valid JSON, pipe to `jq` or consume from any language
- **Dual entry point** — same commands work as CLI _and_ MCP tools with zero adaptation
- **Structured errors** — errors return `{ "error": "...", "code": "..." }`, not stack traces
- **Field filtering** — `--fields id,properties.email` returns only what you need
- **No interactive prompts in automation** — all params available as flags, env vars, or config

## Install

```bash
npm install -g hubspot-cli
```

## Quick Start

```bash
# 1. Create a Private App access token in HubSpot
# Settings -> Integrations -> Private Apps -> Create a private app

# 2. Authenticate with the token
export HUBSPOT_ACCESS_TOKEN=pat-na1-xxxxx

# List contacts
hubspot contacts list --pretty

# Create a contact
hubspot contacts create --email "john@acme.com" --firstname "John" --lastname "Doe"

# Search deals
hubspot deals search --query "acme" --pretty

# Get pipeline stages
hubspot pipelines stages <pipeline-id> --object-type deals
```

## Auth

### 1. Create a Private App access token

1. In HubSpot, go to **Settings** → **Integrations** → **Private Apps** → **Create a private app**
2. In the **Basic info** tab, give it a name such as `hubspot-cli-agent`
3. In the **Scopes** tab, add the scopes for the command groups you plan to use:

| Command group | Scopes required |
|---------------|-----------------|
| `contacts` | `crm.objects.contacts.read` + `crm.objects.contacts.write` |
| `companies` | `crm.objects.companies.read` + `crm.objects.companies.write` |
| `deals` | `crm.objects.deals.read` + `crm.objects.deals.write` |
| `tickets` | `tickets` |
| `owners` | `crm.objects.owners.read` |
| `engagements` (email read) | `sales-email-read` only if you need to read email body content |
| `lists` | `crm.lists.read` + `crm.lists.write` |
| `properties` | `crm.schemas.contacts.read` + `crm.schemas.contacts.write`, `crm.schemas.companies.read` + `crm.schemas.companies.write`, `crm.schemas.deals.read` + `crm.schemas.deals.write` |

4. Click **Create app**
5. Open the **Auth** tab, click **Show token**, and copy the access token. It starts with `pat-na1-` followed by your region.

### 2. Authenticate

Three-tier resolution (highest priority first):

```bash
# Option A: Environment variable (recommended for agents)
export HUBSPOT_ACCESS_TOKEN="pat-na1-xxxxx"

# Option B: Interactive login (saves to ~/.hubspot-cli/config.json)
hubspot login

# Option C: Per-command flag
hubspot contacts list --access-token "pat-na1-xxxxx"
```

### 3. Verify

```bash
hubspot contacts list --limit 1 --pretty
```

## Commands

### CRM Objects (55 commands)

| Group | Commands |
|-------|----------|
| **contacts** | `list`, `get`, `create`, `update`, `delete`, `search`, `merge` |
| **companies** | `list`, `get`, `create`, `update`, `delete`, `search` |
| **deals** | `list`, `get`, `create`, `update`, `delete`, `search` |
| **tickets** | `list`, `get`, `create`, `update`, `delete`, `search` |
| **owners** | `list`, `get` |
| **pipelines** | `list`, `get`, `stages` |
| **engagements** | `create-note`, `create-email`, `create-call`, `create-task`, `create-meeting`, `list`, `get`, `delete` |
| **associations** | `list`, `create`, `delete` |
| **lists** | `list`, `get`, `create`, `update`, `delete`, `add-members`, `remove-members`, `get-members` |
| **properties** | `list`, `get`, `create`, `update`, `delete` |
| **search** | `run` (universal cross-object search) |

### Auth & Config

| Command | Description |
|---------|-------------|
| `login` | Authenticate with access token |
| `logout` | Remove stored credentials |
| `status` | Show current auth + account info |

### MCP Server

```bash
# Start as MCP server (stdio transport)
hubspot mcp
```

Every command is registered as an MCP tool. Configure in Claude Desktop, OpenClaw, or any MCP-compatible agent:

```json
{
  "mcpServers": {
    "hubspot": {
      "command": "npx",
      "args": ["hubspot-cli", "mcp"],
      "env": {
        "HUBSPOT_ACCESS_TOKEN": "pat-na1-xxxxx"
      }
    }
  }
}
```

Or with a local install:
```json
{
  "mcpServers": {
    "hubspot": {
      "command": "node",
      "args": ["/path/to/hubspot-cli/dist/mcp.js"],
      "env": {
        "HUBSPOT_ACCESS_TOKEN": "pat-na1-xxxxx"
      }
    }
  }
}
```

## Global Options

```
--access-token <token>  Override stored auth
--output <format>       json (default) or pretty
--pretty                Shorthand for --output pretty
--quiet                 Suppress output, exit codes only
--fields <fields>       Comma-separated field filter (supports nested: properties.email)
```

## Examples

```bash
# List contacts with specific fields
hubspot contacts list --fields id,properties.email,properties.firstname

# Search companies by name
hubspot companies search --query "acme" --properties name,domain,industry --pretty

# Create a deal in a pipeline stage
hubspot deals create --dealname "Acme Enterprise" --amount "50000" \
  --pipeline "<pipeline-id>" --dealstage "<stage-id>"

# Get all deal pipelines with stages
hubspot pipelines list --object-type deals --pretty

# Log a note
hubspot engagements create-note --body "Discovery call — interested in enterprise plan"

# Create a follow-up task
hubspot engagements create-task --subject "Send proposal" --status NOT_STARTED --priority HIGH

# Associate a contact with a company
hubspot associations create --from-type contacts --from-id 123 \
  --to-type companies --to-id 456 --type-id 1

# Manage a list
hubspot lists create --name "VIP Customers" --processing-type MANUAL
hubspot lists add-members <list-id> --record-ids "123,456"

# Create a custom property
hubspot properties create --object-type contacts --name "lead_score" \
  --label "Lead Score" --type number --field-type number --group contactinformation

# Universal search across any object type
hubspot search run --object-type deals \
  --filter '{"filters":[{"propertyName":"amount","operator":"GT","value":"10000"}]}' \
  --properties dealname,amount,dealstage

# Pipe to jq
hubspot contacts list | jq '.results[].properties.email'
```

## Architecture

This CLI follows the same metadata-driven architecture as [instantly-cli](https://github.com/bcharleson/instantly-cli) (156 commands) and [clay-gtm-cli](https://github.com/bcharleson/clay-gtm-cli):

- **CommandDefinition** — single struct drives both CLI registration and MCP tool registration
- **Zod schemas** — input validation shared between CLI and MCP
- **CRM Object Factory** — `createCrmObjectCommands()` generates CRUD + search for any HubSpot object type
- **Thin REST client** — ~130 lines, cursor-based pagination, exponential backoff on 429s
- **Dual entry** — `dist/index.js` (CLI) and `dist/mcp.js` (MCP server)

## License

MIT
