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

How-to guide

How to Enrich Agile CRM Contacts (With the LeadOcean API)

Agile CRM has no built-in enrichment hook you control. This recipe reads contacts over its REST API, enriches each email in one call and writes title and company back.

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

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 with PUT /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.
bash
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 requests

Step 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.

bash
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.

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"}'

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.

python
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:
        break

Run 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.

python
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 None

Call 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.

bash
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.

code
leadocean_count_leads
  jobLevel: ["VP"], jobFunction: ["Sales & Business Development"],
  country: ["US"], employeeRange: ["11-50"], facets: false

What 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):

json
{
  "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

ErrorCauseFix
401 from LeadOceanBad or missing x-api-keyCheck $LEADOCEAN_API_KEY and the header name
401 from Agile CRMWrong login email or REST keyUse the first key under API & Analytics, with your login email as username
429Above the plan's rate limit (5 requests a second on Free, 200 on Agency)Wait for Retry-After, then retry
402Records spentUpgrade. Free is 1,000 records for life and does not reset
404We hold nothing for that emailSkip it. No record is spent
204 from AgileNo contact for that ID or emailRe-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 →