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

How-to guide

How to Build a LeadOcean, Instantly and HubSpot Outbound Stack (List, Verify, Send)

Export a filtered list from LeadOcean, load it into an Instantly campaign, and write replies back to HubSpot contacts. Sizing the list is free; the CSV export runs on Solo.

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

This stack gives each tool one job. LeadOcean builds the list, Instantly sends the email, and HubSpot holds the contacts and the replies. The free plan sizes the list. The CSV export in step 2 needs Solo.

The example audience is VPs of sales at US companies with 51 to 200 employees. Swap the filters for yours.

TL;DR

  • Time: about 90 minutes for a 200-contact pilot, plus the warm-up time your Instantly mailboxes need before you send.
  • Records: 200 to 400 LeadOcean records for the pilot. Sizing the list first is free. The free plan has 1,000 records, one-off, but bulk CSV export is not on Free, so the pilot export needs Solo.
  • Cost per 10,000 contacts: $97 for LeadOcean Solo plus $97 for Instantly Hypergrowth, about $194 a month at monthly list prices. HubSpot's free CRM covers the contact records for up to 2 users (HubSpot pricing, September 2026).

Prerequisites

  • A LeadOcean key in $LEADOCEAN_API_KEY. Sign up at app.leadocean.io/register. No card needed. Free covers step 1; the step 2 export needs Solo ($97 a month) or Agency ($497 a month).
  • An Instantly account on a plan that fits your list. Growth holds 1,000 uploaded contacts and Hypergrowth holds 25,000 (Instantly pricing, September 2026). Plan-level API access is not spelled out on that page, so confirm it in your workspace. Connect and warm your mailboxes first.
  • An Instantly API key in $INSTANTLY_API_KEY and a campaign ID in $INSTANTLY_CAMPAIGN_ID.
  • A HubSpot account with a private app or static auth token in $HUBSPOT_TOKEN, with scopes to read and write contacts.
  • curl and Python 3 with requests.

Step 1: Size the list before you spend anything

Count first. A search with count=true and limit=1 is free (LeadOcean API reference, September 2026).

bash
curl -X POST "https://api.leadocean.io/v1/people/search?count=true" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["US"],"employeeRange":["51-200"],"emailType":["work"],"emailStatus":["verified"],"limit":1}'

Read meta.total. It is capped at 100,000. Filters join with AND, and several values inside one filter join with OR.

Send only verified addresses to Instantly at first. Bounces hurt the mailbox you just warmed, and LeadOcean does not refund bounced emails.

Step 2: Export the pilot list

Export 200 rows, not the whole audience. Bulk CSV export is included on Solo and Agency, not on Free. The export is asynchronous and reserves records up front (LeadOcean docs, September 2026).

bash
curl -X POST https://api.leadocean.io/v1/exports \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"vp-sales-us-51-200 pilot","filters":{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["US"],"employeeRange":["51-200"],"emailType":["work"],"emailStatus":["verified"]},"limit":200,"columns":["person_id","profile_first_name","profile_last_name","current_job_title","current_company_name","current_company_domain","email_status","email_N_address"],"caps":{"emails":1}}'

The answer is a 202 with an export id. Poll GET /v1/exports/{id} until status is done, then download the CSV.

bash
curl -H "x-api-key: $LEADOCEAN_API_KEY" -o leads.csv \
  https://api.leadocean.io/v1/exports/EXPORT_ID/download

Email columns need a key with the enrich scope. One export takes up to 50,000 rows, and an account takes 500,000 rows a day.

Step 3: Load the CSV into an Instantly campaign

Instantly's bulk endpoint adds up to 1,000 leads per request to a campaign or a list, and it checks emails, blocklists and existing leads on the way in (Instantly API docs, September 2026). A campaign lead needs an email. The script maps the export columns to Instantly's lead fields.

python
import csv, os, requests

API = "https://api.instantly.ai/api/v2/leads/add"
HEAD = {"Authorization": f"Bearer {os.environ['INSTANTLY_API_KEY']}"}

rows = list(csv.DictReader(open("leads.csv")))
leads = [{
    "email": r["email_1_address"],
    "first_name": r["profile_first_name"],
    "last_name": r["profile_last_name"],
    "company_name": r["current_company_name"],
    "website": r["current_company_domain"],
    "job_title": r["current_job_title"],
    "custom_variables": {"leadocean_person_id": r["person_id"]},
} for r in rows if r["email_1_address"]]

for i in range(0, len(leads), 1000):
    res = requests.post(API, headers=HEAD, json={
        "campaign_id": os.environ["INSTANTLY_CAMPAIGN_ID"],
        "skip_if_in_workspace": True,
        "leads": leads[i:i + 1000],
    })
    print(res.status_code, res.json())

skip_if_in_workspace skips anyone already anywhere in your workspace, so a re-run does not double-send. Instantly can also verify addresses on import with verify_leads_on_import. Leave it off here, because LeadOcean already tags each address. Check the Instantly docs for what verification costs on your plan before you turn it on.

The Instantly API allows 100 requests a second and 6,000 a minute for the whole workspace (Instantly API docs, September 2026). A 10,000-contact load is 10 requests.

Before you start the campaign, check the pilot by hand. Open five leads in Instantly and confirm the first name, company and job title landed in the right fields. Check the leadocean_person_id custom variable too. If a merge tag renders empty in your template, fix the column mapping now, not after 10,000 sends.

Keep the first campaign small. Send the 200 contacts over a few days from mailboxes that have finished warm-up, then read the bounce and reply rates in Instantly. A clean pilot is what justifies the larger export.

Step 4: Create the contacts in HubSpot

Upsert the same rows into HubSpot so reps see the people before they reply. The batch upsert creates a contact if the email is new and updates it if not. Use email as the identifier (HubSpot docs, September 2026).

python
import csv, os, requests

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

rows = [r for r in csv.DictReader(open("leads.csv")) if r["email_1_address"]]
for i in range(0, len(rows), 100):
    inputs = [{
        "idProperty": "email",
        "id": r["email_1_address"],
        "properties": {
            "email": r["email_1_address"],
            "firstname": r["profile_first_name"],
            "lastname": r["profile_last_name"],
            "jobtitle": r["current_job_title"],
            "company": r["current_company_name"],
            "website": r["current_company_domain"],
        },
    } for r in rows[i:i + 100]]
    res = requests.post(URL, headers=HEAD, json={"inputs": inputs})
    print(res.status_code)

We batch 100 at a time as our own choice. List your portal's property names with GET /crm/v3/properties/0-1 before you map more fields. For enumeration properties, use the internal names, not the labels.

A privately distributed app on a Free or Starter account is limited to 100 calls per app per 10 seconds and 250,000 a day (HubSpot docs, September 2026). A 10,000-row load is 100 calls.

Step 5: Write replies back to HubSpot

Instantly sends webhook events with event_type, lead_email and campaign_name (Instantly API docs, September 2026). The event types include reply_received, lead_interested, email_bounced and lead_unsubscribed. This handler moves interested leads forward in HubSpot.

python
import os, requests
from http.server import BaseHTTPRequestHandler, HTTPServer
import json

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

class Hook(BaseHTTPRequestHandler):
    def do_POST(self):
        ev = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        if ev.get("event_type") == "lead_interested" and ev.get("lead_email"):
            requests.post(URL, headers=HEAD, json={"inputs": [{
                "idProperty": "email",
                "id": ev["lead_email"],
                "properties": {"lifecyclestage": "salesqualifiedlead"},
            }]})
        self.send_response(200)
        self.end_headers()

HTTPServer(("", 8080), Hook).serve_forever()

Point an Instantly webhook at a public URL that runs this handler. Add a shared secret check before you use it in production. Treat email_bounced and lead_unsubscribed the same way: write them to a HubSpot property so no rep emails that person by hand.

Step 6: Cost a 10,000-contact run and know where each tool stops

Price the run before you scale. This script is arithmetic from list prices, not a quote.

python
contacts = 10_000
leadocean_solo = 97        # flat per month, fair-use pace, no per-record price
instantly_hyper = 97       # monthly list, holds 25,000 uploaded contacts
hubspot_free_crm = 0       # free CRM, up to 2 users

assert contacts <= 25_000  # Hypergrowth upload cap

total = leadocean_solo + instantly_hyper + hubspot_free_crm
print("Monthly list total:", total)
print("Per contact:", round(total / contacts, 4))

It prints $194 a month and about $0.0194 a contact. The inputs come from public pages. Instantly lists Hypergrowth at $97 a month with 25,000 uploaded contacts (Instantly pricing, September 2026). Its plan card says 125,000 emails a month and its comparison table says 100,000, so plan sends from the lower number. The page shows a yearly toggle, but our check did not capture a yearly figure, so this page prices monthly only.

HubSpot's free tier is free for up to 2 users with no card. Paid Sales Hub tiers start at Starter, priced per seat. The pricing page shows prices in your local currency, so read the Starter figure there (HubSpot pricing, October 2026). LeadOcean Solo has no per-record price, only a fair-use pace, and 10,000 contacts use 10,000 to 20,000 records. Agency is $497 a month if a team shares keys. See pricing.

To scale from the pilot to 10,000 contacts, change two numbers. Raise limit in the step 2 export, up to 50,000 rows a request. Then run steps 3 and 4 on the new CSV. The Instantly script loads 1,000 leads per request and the HubSpot script loads 100, so the load is 10 and 100 calls. Both fit inside the rate limits above.

Watch the sending side more than the loading side. Ten thousand contacts with a three-step sequence is about 30,000 emails, which fits inside the lower 100,000 a month figure with room for follow-ups. Spread them across several mailboxes rather than one.

Where each tool stops:

  • LeadOcean stops at the list. It has no sequencer, dialer, CRM or Chrome extension. email_status is a signal, and bounced emails are not refunded.
  • Instantly stops at sending and the inbox. It does not supply contact data in this recipe, and your upload cap is set by the plan.
  • HubSpot stops at the record. It does not send this cold email, and its API limits depend on your tier.

What you get

Step 1 is a free count. We ran it through LeadOcean's MCP leadocean_count_leads tool on 2026-10-21.

text
jobLevel VP, jobFunction Sales & Business Development, country US,
employeeRange 51-200, emailType work, emailStatus verified
total 4027   totalIsExact true   distinctCompanies 3052   credits 0

That is a count of verified work emails only. It is not every VP of sales in that size band. Your mix will differ.

The export create call answers with the shape below. This is the response shape from the API reference (openapi.json), with placeholder values.

json
{
  "success": true,
  "data": {
    "id": "EXPORT_ID",
    "name": "vp-sales-us-51-200 pilot",
    "status": "queued",
    "requested": 200,
    "estimatedTotal": 4027,
    "perRow": 1,
    "remaining": null,
    "pollAfter": 5
  }
}

requested is the records reserved and perRow is 1 or 2. remaining is null on a paid plan, which has a pace rather than a balance. Read perRow before you scale up. Instantly's bulk endpoint then answers with a summary that reports total_sent and leads_uploaded, so you can see how many rows it skipped.

Troubleshooting

ErrorCauseFix
401 "Missing or malformed x-api-key"The LeadOcean header is missing or wrong.Send x-api-key: $LEADOCEAN_API_KEY.
402 on export createFewer records are left than the export reserves.Lower limit. On Free the 1,000 records are one-off, and bulk CSV export is not on Free.
429 from LeadOceanMore requests a second than your plan allows (5 on Free, 50 on Solo, 200 on Agency), or a daily export ceiling.Honour Retry-After. Exports allow 500,000 rows a day.
Zero rows backA filter value is outside the catalogue, such as United States for US.Read the valid values from GET /v1/enums.
400 on export createlimit is over 50,000, or a filter cannot be applied.Split into several exports with narrower filters.
429 from InstantlyThe workspace passed 100 requests a second or 6,000 a minute.Batch up to 1,000 leads a request and pause between batches (Instantly API docs, September 2026).

FAQ

Do I need HubSpot in this stack?

No. Instantly sends and tracks replies without it. HubSpot earns its place when reps already work from it and you want interested leads in the pipeline. The Lemlist and HubSpot stack wires the same CRM to a different sender.

Why verified emails only?

Cold sending punishes bounces. Starting with verified keeps the first campaign clean. Add catch_all_valid later, and check the rest with a separate verifier such as the VerifyHQ and ReachHQ stack.

How many records does a full run cost?

One export row costs one record, or two when the create response shows perRow of 2. Solo at $97 a month and Agency at $497 a month have no per-record price, only a fair-use pace. See pricing.

Can the same list feed more than one campaign?

Yes. Export once, then split the CSV by title or company size and load each slice into its own Instantly campaign. Keep skip_if_in_workspace on so one person never sits in two campaigns at once.

Can I use a different CRM?

Yes. The Smartlead and Pipedrive stack shows the same list step feeding another sender and CRM. Back to the blog hub.

Build your first LeadOcean list for Instantly and HubSpot. Start free.

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

Get your free API key →