TL;DR
- Read contacts from the Agile CRM REST API, send each email to
POST /v1/people/enrich, and write job title and company back withPUT /dev/api/contacts/edit-properties. - It takes about 30 minutes. A 100-contact test run spends at most 100 of your 1,000 free records.
- Contacts we hold nothing for return a 404 and cost nothing.
This is not a six-tool test roundup. We did not run six vendors against an Agile CRM account, so we will not claim a ranking. For a comparison of enrichment tools by CRM, read our CRM enrichment roundup and the shortlist of 8 tools. This page is the recipe a developer can run today.
Prerequisites
- A LeadOcean API key from app.leadocean.io. The free plan has no card and 1,000 records, one-off. Free keys can search and enrich.
- Your Agile CRM domain, login email and REST API key. Agile's API docs (October 2026) say to use the first key under Admin Settings, API & Analytics. Requests use HTTP basic auth with your email as the username and the key as the password.
- Python 3 with
requests.
export LEADOCEAN_API_KEY="paste-your-key-here"
export AGILE_DOMAIN="yourteam"
export AGILE_EMAIL="you@example.com"
export AGILE_API_KEY="paste-agile-rest-key-here"
pip install requestsStep 1: Read a page of contacts from Agile CRM
Agile pages contacts with page_size and cursor. Per its docs, send Accept: application/json, or the response is XML. The cursor for the next page sits on the last contact, and no cursor means the end of the list.
curl -s "https://$AGILE_DOMAIN.agilecrm.com/dev/api/contacts?page_size=20" \
-H "Accept: application/json" \
-u "$AGILE_EMAIL:$AGILE_API_KEY"Each contact has an id and a properties list. Email lives in the property named email, with a subtype of work or personal.
Step 2: Enrich one email with LeadOcean
Send one identifier per call. A work email is the strongest key, because it round-trips to the same person.
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"}'You get the profile, the current role in contact_current_experiences[] and, since October 2026, career history in resume_data. Past employers carry no company_id yet. Contact points are returned when you pass reveal_email, which costs no extra record. The API has no name-plus-domain lookup, so contacts without an email need a LinkedIn URL.
Step 3: Loop, enrich and write back
This script walks the contacts, enriches each work email and updates only title and company. The Agile edit-properties call adds or updates the properties you send and leaves other fields alone, per its docs. It cannot delete properties.
import os, time, requests
AG = f"https://{os.environ['AGILE_DOMAIN']}.agilecrm.com/dev/api"
AUTH = (os.environ["AGILE_EMAIL"], os.environ["AGILE_API_KEY"])
LO = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
def work_email(contact):
for p in contact["properties"]:
if p["name"] == "email" and p.get("subtype") == "work":
return p["value"]
def enrich(email):
r = requests.post("https://api.leadocean.io/v1/people/enrich",
headers=LO, json={"email": email})
if r.status_code == 404:
return None # not in our data, costs nothing
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 1)))
return enrich(email)
r.raise_for_status()
return r.json()["data"]
def update(contact_id, title, company):
props = [{"type": "SYSTEM", "name": "title", "value": title},
{"type": "SYSTEM", "name": "company", "value": company}]
requests.put(f"{AG}/contacts/edit-properties",
auth=AUTH, headers={"Accept": "application/json"},
json={"id": contact_id, "properties": props}).raise_for_status()
WRITE = False # dry run first
cursor, done = None, 0
while done < 100: # test run: 100 contacts
params = {"page_size": 20, **({"cursor": cursor} if cursor else {})}
page = requests.get(f"{AG}/contacts", auth=AUTH, params=params,
headers={"Accept": "application/json"}).json()
for c in page:
email = work_email(c)
person = enrich(email) if email else None
if person:
role = (person["contact_data"].get("contact_current_experiences") or [{}])[0]
print(c["id"], email, role.get("job_title"), role.get("company_name"))
if WRITE and role.get("job_title"):
update(c["id"], role["job_title"], role.get("company_name") or "")
done += 1
cursor = page[-1].get("cursor") if page else None
if not cursor:
breakRun it once with WRITE = False and read the printed titles and companies. The current role carries job_title, company_name and company_domain, per the LeadOcean enrich docs. Then set WRITE = True. Agile treats all values as case-sensitive, so do not normalise the case of existing data.
Step 4: Keep the run safe to repeat
Enrichment jobs get re-run, so make each pass cheap. Store the Agile contact id and the date you enriched it in a small SQLite table. Skip any contact enriched in the last 30 days, because the dataset refreshes monthly and an earlier re-run returns the same answer.
import sqlite3
db = sqlite3.connect("ledger.db")
db.execute("create table if not exists seen (id text primary key, at text)")
def due(contact_id):
row = db.execute("select 1 from seen where id=? and at > date('now','-30 day')",
(contact_id,)).fetchone()
return row is NoneCall due(c["id"]) before enrich(), and insert the row after a successful write-back. Count your misses too. A 404 costs nothing, but a high miss rate tells you the CRM holds personal addresses or stale emails. For those, enrich by LinkedIn URL instead, with {"linkedin_url": "https://www.linkedin.com/in/jane-doe"}.
Step 5: Add new contacts instead of only fixing old ones
Enrichment fixes what you hold. Search finds what you lack. Search takes a flat JSON body, filters are arrays, limit runs 1 to 100, and you page with cursor. One search pages to 10,000 rows, so narrow the filters rather than walking the index.
curl -s -X POST https://api.leadocean.io/v1/people/search \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jobLevel": ["VP"], "jobFunction": ["Sales & Business Development"],
"country": ["US"], "employeeRange": ["11-50"], "limit": 25}'That page spends 25 records. Filter values are exact strings, so resolve them with leadocean_list_enum_values before you guess. For a large list, POST /v1/exports takes up to 50,000 rows a request and 500,000 a day, and bulk CSV export is not on Free.
Step 6: Size the next list for free
Before you buy more records, count what your ideal buyers look like. leadocean_count_leads is free through the MCP server, and GET /v1/companies/count sizes company lists. Here is a count run on 21 October 2026.
leadocean_count_leads
jobLevel: ["VP"], jobFunction: ["Sales & Business Development"],
country: ["US"], employeeRange: ["11-50"], facets: falseWhat you get
The count above returned 20,836 people, counted as mailable (emailStatus verified, catch_all_valid or catch_all, the tool default). Without the email condition the same filters match 69,766. They span 16,102 employers. Run on 21 October 2026, cost 0 records.
The enrich call returns this shape (response shape from the API reference, placeholder values):
{
"success": true,
"data": {
"profile_data": {},
"contact_data": {
"has_email": true,
"has_phone": true,
"email_status": "verified",
"email_type": "work",
"contact_current_experiences": [
{ "company_name": "Acme", "company_domain": "acme.com", "job_title": "VP Sales" }
]
},
"resume_data": {}
},
"meta": {}
}Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 from LeadOcean | Bad or missing x-api-key | Check $LEADOCEAN_API_KEY and the header name |
| 401 from Agile CRM | Wrong login email or REST key | Use the first key under API & Analytics, with your login email as username |
| 429 | Above the plan's rate limit (5 requests a second on Free, 200 on Agency) | Wait for Retry-After, then retry |
| 402 | Records spent | Upgrade. Free is 1,000 records for life and does not reset |
| 404 | We hold nothing for that email | Skip it. No record is spent |
| 204 from Agile | No contact for that ID or email | Re-read the contact list |
FAQ
Does Agile CRM have native enrichment?
We could not confirm a native enrichment feature in the REST API docs, which list contacts, companies, deals, notes and tasks. Check Agile's own product pages before you rely on that.
How many records will a full run cost?
One per contact we hold. Free covers 1,000. Beyond that, Solo at $97 a month fits one person. Agency at $497 a month fits teams.
Can I enrich by company domain?
Yes. GET /v1/companies/{domain}/people lists people at a domain, and POST /v1/companies/enrich returns the company record.
How often does the data refresh?
The dataset refreshes monthly. Each record carries its own fetched_at date.
See also CRM enrichment use cases.
Enrich your Agile CRM contacts on the free plan
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →