TL;DR
- Nutshell sends a webhook when a contact changes. Fetch the contact, call
POST /v1/people/enrichonce, and PATCH the result back. Any of five runners can do it. - Setup takes about 45 minutes for the first runner. Each extra runner takes about 10.
- It spends one LeadOcean record per contact we hold. A miss is a 404 and costs nothing, so the free 1,000 covers a real test.
Nutshell's own docs describe webhooks, a REST API and a Zapier app. They do not describe a built-in enrichment step. So "autopilot" here means a trigger, a lookup and a write-back that you wire up once. Nutshell API access is open to all customers regardless of plan (Nutshell developer hub, September 2026).
The five runners are the same recipe in different clothes:
- A small script that receives the Nutshell webhook.
- Zapier, with the Nutshell app and a Webhooks step.
- n8n, with a Webhook node and two HTTP Request nodes.
- Make, with a custom webhook and two HTTP modules.
- A cron job that sweeps contacts missing an email.
Steps 1 to 4 build the core. Step 5 maps it onto each runner.
Prerequisites
- A Nutshell account with an API key from Setup > API keys (Nutshell docs, September 2026). Nutshell uses HTTP Basic auth: your user email is the username and the key is the password.
- A LeadOcean key from app.leadocean.io. The free plan has no card, 1,000 records for life and 5 requests a second.
- Both secrets as shell variables. Never paste them into a page or a repo.
export LEADOCEAN_API_KEY="paste-your-leadocean-key"
export NUTSHELL_USER="you@acme.com"
export NUTSHELL_API_KEY="paste-your-nutshell-key"Step 1: Size the job before you spend anything
Count first. A count spends no records. This is a real call from 2026-10-21, made with the MCP tool leadocean_count_leads.
{
"jobLevel": ["C-Team"],
"employeeRange": ["11-50"],
"country": ["US"],
"facets": false
}It returned 732,580 people with a verified, catch_all_valid or catch_all email, out of 2,808,968 with no email condition. That is a mailable count, not everyone matching. Use it to see whether a list you plan to add to Nutshell is worth buying into. For your existing contacts, the cost is simply the number of contacts you enrich.
Step 2: Catch the Nutshell webhook
Create the webhook under Setup > API keys. All webhooks receive every event, so filter on your side (Nutshell docs, September 2026). Nutshell recommends treating the payload as a prompt. Fetch the changed entity with the REST API instead of trusting the payload fields (Nutshell docs, September 2026).
Fetch the contact with Basic auth:
curl -s -u "$NUTSHELL_USER:$NUTSHELL_API_KEY" \
"https://app.nutshell.com/rest/contacts/123-contacts" \
-H "accept: */*"Contacts carry an emails array with isPrimary, name and value. They also carry a description shown under the name (Nutshell API reference, September 2026). The 123-contacts ID is a placeholder in the documented {integer}-{entityType} format.
Skip contacts that already have a description. That stops the loop from re-enriching every contact it edits itself.
Step 3: Enrich by email
Send exactly one identifier. email, linkedin_url, phone or person_id all work. Two identifiers return an error (LeadOcean docs, September 2026).
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}'reveal_email costs no extra record. A 404 means we hold nothing for that person and costs nothing. Stop there and leave the Nutshell contact alone.
Step 4: Write the result back to Nutshell
Nutshell updates use JSON Patch with the content type application/json-patch+json (Nutshell quickstart, September 2026). The quickstart shows a replace on /contacts/0/firstName. The same shape works for description.
curl -s -X PATCH "https://app.nutshell.com/rest/contacts/123-contacts" \
-u "$NUTSHELL_USER:$NUTSHELL_API_KEY" \
-H "content-type: application/json-patch+json" \
-d '[{"op":"replace","path":"/contacts/0/description","value":"VP Sales at Acme"}]'Build the description from contact_data.contact_current_experiences[0].job_title and company_name. Check contact_emails[].status before you add an address, and only keep verified or catch_all_valid. Phones come back as contact_phones[].phone. This recipe writes only the description; map a phone field from the Nutshell API reference before you add one.
Here is the whole loop as one Python function:
import os, requests
LO = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
NS = (os.environ["NUTSHELL_USER"], os.environ["NUTSHELL_API_KEY"])
def handle(contact_id: str) -> None:
r = requests.get(f"https://app.nutshell.com/rest/contacts/{contact_id}", auth=NS)
contact = r.json()["contacts"][0]
if contact.get("description"):
return
email = next((e["value"] for e in contact.get("emails", []) if e.get("isPrimary")), None)
if not email:
return
p = requests.post("https://api.leadocean.io/v1/people/enrich",
headers=LO, json={"email": email, "reveal_email": True})
if p.status_code == 404:
return
p.raise_for_status()
jobs = p.json()["data"]["contact_data"].get("contact_current_experiences") or []
if not jobs:
return
text = f'{jobs[0]["job_title"]} at {jobs[0]["company_name"]}'
requests.patch(f"https://app.nutshell.com/rest/contacts/{contact_id}", auth=NS,
headers={"content-type": "application/json-patch+json"},
json=[{"op": "replace", "path": "/contacts/0/description", "value": text}])Step 5: Pick a runner
All five call the same two APIs. They differ in who hosts the trigger.
| Runner | Trigger | What you build |
|---|---|---|
| Your own script | Nutshell webhook posts to your URL | handle() above behind a small web server |
| Zapier | Nutshell app trigger | Webhooks by Zapier, Custom Request, to the enrich URL, then a Nutshell update |
| n8n | Webhook node | HTTP Request to Nutshell, HTTP Request to LeadOcean, HTTP Request PATCH |
| Make | Custom webhook | Two HTTP modules and a filter on status 404 |
| Cron | None, a schedule | The sweep below |
Zapier is the fastest point-and-click route. Nutshell lists a Zapier app that creates, updates and searches people, companies and leads (Nutshell, September 2026). LeadOcean has no listing in any of these tools, so the enrich call is a plain HTTP request with an x-api-key header. Our n8n recipe and Zapier recipe show the node and step settings.
For the cron runner, list contacts with an empty description and call handle() on each. Stay under your plan's rate limit (5 requests a second on Free, 200 on Agency).
0 6 * * * /usr/bin/python3 /opt/enrich/sweep.py >> /var/log/enrich.log 2>&1What you get
The response shape from the API reference, with placeholder values (not a real person).
{
"success": true,
"data": {
"profile_data": { "...": "Jane Doe" },
"contact_data": {
"contact_current_experiences": [
{ "job_title": "VP Sales", "company_name": "Acme", "company_domain": "acme.com" }
],
"contact_emails": [
{ "email": "jane.doe@acme.com", "type": "work", "status": "verified" }
],
"contact_phones": [
{ "phone": "+1 415 555 0133" }
]
}
}
}Since 17 October 2026 the response can also carry career history in resume_data: past roles, education and certifications, each when held.
After the run the Nutshell contact reads "VP Sales at Acme" under the name. Your reps filter on it instead of guessing from a bare email.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 from LeadOcean | Missing or wrong x-api-key | Re-copy the key and check the header name |
| 401 from Nutshell | Wrong email or API key in Basic auth | The username is the user's email, the password is the API key |
| 404 from enrich | We hold nothing for that identifier | Skip the contact. It cost nothing |
| 429 | Above the plan's rate limit (5 a second on Free, 200 on Agency) | Honor Retry-After and queue the calls |
| 402 | Free records are spent | Move to Solo at $97 a month or Agency at $497 a month |
| Loop never stops | Your PATCH fires a new Nutshell webhook | Skip contacts that already have a description |
FAQ
Does Nutshell enrich contacts by itself?
Nutshell's developer docs describe webhooks, REST, JSON-RPC and Zapier. They show no built-in enrichment step (Nutshell developer hub, September 2026). Check your plan in the app before you assume one exists.
Which runner should I pick?
Pick Zapier if nobody on the team writes code. Pick your own script or n8n if you want no per-task fee and full control of the retry logic. Pick cron if you only need a nightly sweep.
What does it cost?
The Free plan gives 1,000 records once, with no card. Solo is $97 a month and Agency is $497 a month, with a fair-use pace instead of credits. See pricing. For the wider picture, read what data enrichment actually costs.
Will it overwrite what my reps typed?
Not with the guard in Step 2. The function returns early when a description exists. Extend the same check to any field you want to protect.
Are there other CRMs I can do this for?
Yes. The shape is identical. See which enrichment tools work with Zendesk and the roundup of 9 B2B enrichment tools.
Put your Nutshell contacts on autopilot
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →