Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

How-to guide

How to Automate HubSpot with Claude Code (10 Workflows)

Size a list for free, enrich or find contacts with LeadOcean, then let Claude Code upsert them into HubSpot in batches of 100.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

Claude Code HubSpot automation works best as a script that Claude Code writes and you run: LeadOcean supplies the people, HubSpot's batch upsert writes them. Keep the chat for design and the script for volume.

This guide builds the first workflow end to end, then lists nine more that reuse the same three pieces.

TL;DR

  • It takes about 30 minutes the first time. Each later workflow is a new filter and a new property map.
  • Sizing a list is free. Each person you search or enrich spends 1 record. A 50-contact test costs up to 100 of the 1,000 free records.
  • HubSpot batch operations take 100 records per request (HubSpot docs, October 2026), so 1,000 contacts is 10 write calls.

Prerequisites

  • A LeadOcean API key from app.leadocean.io, stored as $LEADOCEAN_API_KEY. Free has no card and 5 requests a second.
  • A HubSpot private app token in $HUBSPOT_TOKEN, with scopes to read and write contacts.
  • Claude Code, curl, jq and Python 3 with requests.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"
export HUBSPOT_TOKEN="paste-your-token-here"
claude mcp add --transport http leadocean https://api.leadocean.io/mcp

The last line adds the LeadOcean MCP server, which signs in with OAuth and needs no pasted key. Setup details are on the Claude Code MCP page.

Step 1: Size the list before you spend anything

Ask Claude Code to count first. The leadocean_count_leads tool is free however often you call it.

text
Use leadocean_count_leads with title "Revenue Operations", jobLevel Director, VP and Manager, country US. Set facets=false. Tell me the total and totalAll.

Claude Code calls the tool with this input:

json
{
  "title": ["Revenue Operations"],
  "jobLevel": ["Director", "VP", "Manager"],
  "country": ["US"],
  "facets": false
}

Enum values are exact strings. If a filter returns nothing, check it with leadocean_list_enum_values before you retry.

Step 2: Pull the people with the search endpoint

Search takes a flat JSON body. Filters are arrays, limit runs from 1 to 100, and cursor pages the rest.

bash
curl -s https://api.leadocean.io/v1/people/search \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "content-type: application/json" \
  -d '{"title":["Revenue Operations"],"jobLevel":["Director","VP","Manager"],"country":["US"],"limit":50}' \
  | tee page1.json | jq '.meta'

Each person returned is 1 record. One search pages to 10,000 rows, and a capped page has meta.depthCapped set. Narrow the filters instead of walking further.

Before you write the mapping, look at one row:

bash
jq '.data[0] | keys' page1.json

The documented top-level keys are profile_data, contact_data, resume_data and meta. Search rows carry no email addresses, only the has_email and email_status flags. Each row's meta.source_ids.person_id is what you enrich in Step 3 to get the address.

Step 3: Let Claude Code write the HubSpot upsert

Give Claude Code the shape and the rules. It writes the script, you review it, then you run it.

text
Read page1.json. Write upsert.py that, for each row with has_email true,
POSTs meta.source_ids.person_id to /v1/people/enrich with reveal_email true.
Take the first contact_data.contact_emails[] entry with type "work" and status
verified or catch_all_valid. Map email, firstname, lastname, jobtitle
(contact_current_experiences[0].job_title) and company (its company_name).
Post to /crm/v3/objects/contacts/batch/upsert in chunks of 100 with
idProperty "email". Cache every response. Print counts. No hardcoded tokens.

The reviewed HubSpot half:

python
import os, requests

HEAD = {"Authorization": f"Bearer {os.environ['HUBSPOT_TOKEN']}"}
URL = "https://api.hubspot.com/crm/v3/objects/contacts/batch/upsert"

def upsert(rows):
    for i in range(0, len(rows), 100):
        chunk = rows[i:i + 100]
        body = {"inputs": [
            {"idProperty": "email", "id": r["email"], "properties": r}
            for r in chunk
        ]}
        resp = requests.post(URL, headers=HEAD, json=body)
        resp.raise_for_status()
        print(i, resp.status_code)

rows = [{"email": "jane.doe@acme.com", "firstname": "Jane",
        "lastname": "Doe", "jobtitle": "Director of RevOps",
        "company": "Acme"}]
upsert(rows)

HubSpot says partial upserts are not supported with email as the idProperty, so send the full property set each time (HubSpot docs, October 2026). Use a custom unique property if you need partial updates.

Run it on 50 rows first. Open five of the new contacts in HubSpot and check the title, company and email against the source. Only then raise the limit.

Each revealed address sits in contact_data.contact_emails[] with its own type and status (enrich docs). Keep work addresses with a sendable status, because bounced emails are not refunded.

Step 4: Enrich the contacts you already have

Enrichment takes exactly one of linkedin_url, email, phone or person_id. An enrich that finds nobody is a 404 and costs nothing.

bash
curl -s -X POST https://api.leadocean.io/v1/people/enrich \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "content-type: application/json" \
  -d '{"email":"jane.doe@acme.com","reveal_email":true}'

Since October 2026 the response also carries career history in resume_data: past roles, education, certifications and awards. Past employers have no company_id yet. Phones come back in contact_phones[].phone.

To pull the contacts to enrich, read them from HubSpot first. The batch read takes up to 100 contacts by email with idProperty set to email.

Two habits keep this cheap. Cache every API response to a local file, so a failed HubSpot write never makes you re-spend records. And log the count of rows fetched, created and updated on every run, so you can explain any change in usage.

Step 5: Run it on a schedule, with a guard

Ask Claude Code to wrap the script with two guards: a record budget and a rate guard. HubSpot answers 429 above its limits, and a privately distributed app has its own daily cap (HubSpot docs, September 2026).

text
Add to upsert.py: stop if more than 500 people would be fetched in one run, sleep 0.25s between LeadOcean calls on the Free plan, and retry HubSpot 429s after the Retry-After header.

Then run it by cron or a GitHub Action. Do not give the scheduled job a bigger budget than the plan's pace.

The 10 workflows

The first workflow is the persona pull you just built. The rest change one thing: the LeadOcean call, the HubSpot write, or both.

Each one is a new filter set plus a property map. Size it with leadocean_count_leads first.

#WorkflowLeadOcean callHubSpot write
1Net-new contacts for a persona/v1/people/searchbatch upsert
2Fill missing emails on old contacts/v1/people/enrich by linkedin_urlbatch update
3Fill missing phonesGET /v2/people/phonebatch update
4Fill job title and levelenrich, contact_current_experiences[]batch update
5Buying committee for one accountGET /v1/companies/{domain}/peoplebatch upsert
6Size a territoryGET /v1/companies/count (free)none
7Company firmographicsPOST /v1/companies/enrichbatch update
8Career history note on a contactenrich, resume_datanotes or a custom property
9Flag stale recordsenrich by email, compare titlea property flag
10Bulk list over 10,000 rowsPOST /v1/exports (not on Free)CSV import

Workflows 2, 3, 4 and 9 start from contacts already in HubSpot, so they spend records only on people we hold. Workflow 6 spends nothing at all. Use it before any territory plan, then pass the same filters to workflow 1.

Exports take up to 50,000 rows a request and 500,000 a day. Workflows 1 to 9 spend 1 record per person or company returned.

What you get

Output of the count in Step 1, run 2026-10-21 with leadocean_count_leads:

json
{
  "total": 5749,
  "totalIsExact": true,
  "totalAll": 10284,
  "distinctCompanies": 3521,
  "credits": 0
}

That is 5,749 people with a verified, catch-all valid or catch-all email, out of 10,284 matching the same filters. The count describes the mailable audience, not everyone.

Enrich response shape from the API reference, with placeholder values:

json
{
  "success": true,
  "data": {
    "profile_data": { "profile_full_name": "Jane Doe" },
    "contact_data": {
      "has_email": true,
      "email_status": "verified",
      "contact_emails": [
        { "email": "jane.doe@acme.com", "type": "work", "status": "verified" }
      ],
      "contact_current_experiences": [
        { "job_title": "Director of RevOps", "company_name": "Acme", "company_domain": "acme.com" }
      ]
    },
    "resume_data": { "experiences": [] }
  },
  "meta": { "credits": 1 }
}

contact_emails and contact_phones appear only with reveal_email: true. Run Step 3 on one row to see real values in your own account.

Troubleshooting

ErrorCauseFix
401 from LeadOceanMissing or wrong x-api-keyRe-export $LEADOCEAN_API_KEY and check the header name
429 from LeadOceanAbove the plan rate limit: 5 a second on Free, 200 on AgencyWait the Retry-After seconds, then slow the loop
402 from LeadOceanThe 1,000 free records are spentMove to Solo at $97 a month or Agency at $497 a month, see pricing
Empty dataEnum value not exact, such as director instead of DirectorCheck with leadocean_list_enum_values
404 on enrichWe hold nothing for that personNothing is charged. Try another key such as linkedin_url
Export refusedOver 50,000 rows a request or 500,000 a day, or on FreeSplit the export, or use a paid plan

FAQ

Can Claude Code write to HubSpot directly?

Yes, through scripts that call HubSpot's REST API with your token. This guide uses the contacts API only. Review the script before you run it.

How many records does the first run cost?

Counting is free. A 50-person search spends 50 records, and enriching them to get emails spends up to 50 more. Enriching a contact that we hold nothing for spends none.

Does LeadOcean replace HubSpot?

No. LeadOcean has no CRM, sequencer or dialer. It is the data source, and HubSpot stays the system of record.

Do I need the MCP server or the API?

The MCP server is best for sizing and one-off questions. The API is better for scheduled jobs.

Which plan fits a team?

One person doing this weekly fits Solo at $97 a month. A RevOps team with several keys fits Agency at $497 a month.

Fill HubSpot from one data source

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →