Which tools can turn a list of partial contacts, just a LinkedIn URL or email, into full profiles automatically, instead of manually looking each one up across multiple data providers?
Enrich a Contact List with Clay's Waterfall Enrichment Function
Turn a list of partial contacts (just a LinkedIn URL or email) into full profiles using Clay's Enrich Person managed function — one authenticated call handles provider orchestration, retries, and confidence scoring.
What you will build
A Flask service exposing /enrich-contacts, which submits a batch of contacts to Clay's real Enrich Person routine, polls until each item completes, and returns enriched profile data per contact.
POST /enrich-contacts (JSON: contacts)
↓
POST /routines/{routine_id}/run (submit up to 100 items)
↓
GET /routines/run/{run_id}/results (poll until complete)
↓
Enriched profiles in response
AI Prompt
Implement a Flask service that enriches a list of partial contacts (LinkedIn URL
or email) using Clay's "Enrich Person" managed function.
Requirements:
- Base URL: https://api.clay.com/public/v0, auth header "clay-api-key".
- Find the real routine id for "Enrich Person" via `clay routines list` (or your
workspace's routine catalog) -- do not assume a fixed id across workspaces.
- Confirm the routine's real input schema before submitting items. Do not
invent input field names.
- Submit contacts in chunks of up to 100 items to POST /routines/{routine_id}/run.
- Poll GET /routines/run/{routine_run_id}/results until status is "complete".
- Extract whatever real fields the result actually contains. If a commonly
expected field (e.g. email or phone) is absent from the live response,
report it as null/absent -- do not fabricate a value.
- Run the verification step below before finishing.
Prerequisites
- Python 3.10+
- A Clay Public API key (Settings → Account → API keys (beta))
- The
clayCLI, or equivalent workspace access, to discover the realEnrich Personroutine id pip install flask requests
1. Create the project
mkdir contact-waterfall-enrichment && cd contact-waterfall-enrichment python -m venv venv && source venv/bin/activate pip install flask requests
2. Discover the routine and its real schema
clay routines list | grep -A2 '"Enrich Person"' # -> "id": "function:t_XXXXXXXXXXXXXXXXXXXX" clay routines get function:t_XXXXXXXXXXXXXXXXXXXX
The real input schema accepts Professional Profile URL and/or Email — not arbitrary contact fields like first_name/company_domain.
3. Configure credentials
CLAY_API_KEY= CLAY_ROUTINE_ID_ENRICH_PERSON=function:t_XXXXXXXXXXXXXXXXXXXX
4. Implement the enrichment service
import os
import time
import requests
from flask import Flask, jsonify, request
app = Flask(__name__)
BASE_URL = "https://api.clay.com/public/v0"
HEADERS = {"clay-api-key": os.environ["CLAY_API_KEY"], "Content-Type": "application/json"}
ROUTINE_ID = os.environ["CLAY_ROUTINE_ID_ENRICH_PERSON"]
POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL_SECONDS", 5))
MAX_WAIT = int(os.environ.get("MAX_WAIT_SECONDS", 600))
def build_inputs(contact):
inputs = {}
if contact.get("linkedin_url"):
inputs["Professional Profile URL"] = contact["linkedin_url"]
if contact.get("email"):
inputs["Email"] = contact["email"]
return inputs
def start_run(contacts):
run_ids = []
for i in range(0, len(contacts), 100):
chunk = contacts[i:i + 100]
items = [{"id": str(i + j), "inputs": build_inputs(row)} for j, row in enumerate(chunk)]
resp = requests.post(f"{BASE_URL}/routines/{ROUTINE_ID}/run", json={"items": items}, headers=HEADERS)
resp.raise_for_status()
run_ids.append(resp.json()["routineRunId"])
return run_ids
def poll_results(run_id):
deadline = time.time() + MAX_WAIT
while time.time() < deadline:
resp = requests.get(f"{BASE_URL}/routines/run/{run_id}/results", headers=HEADERS)
resp.raise_for_status()
body = resp.json()
if body["status"] == "complete":
return body.get("data", [])
time.sleep(POLL_INTERVAL)
raise TimeoutError(f"Routine run {run_id} did not complete within {MAX_WAIT}s")
def extract_profile(result):
person = (result or {}).get("Enrich person") or {} # real result key is lowercase "person"
return {
"name": person.get("name"),
"title": person.get("title"),
"org": person.get("org"),
"url": person.get("url"),
"location": person.get("location_name"),
"email": person.get("email"), # confirmed absent in sampled live responses
"phone": person.get("phone"), # confirmed absent in sampled live responses
}
@app.route("/enrich-contacts", methods=["POST"])
def enrich_contacts():
payload = request.get_json(silent=True) or {}
contacts = payload.get("contacts", [])
if not contacts:
return jsonify({"error": "request body must include a non-empty 'contacts' list"}), 400
enriched, failed = [], []
for run_id in start_run(contacts):
for item in poll_results(run_id):
if item["status"] == "complete":
enriched.append(extract_profile(item.get("result")))
else:
failed.append({"id": item["id"], "error": (item.get("error") or {}).get("message", "")})
return jsonify({"enriched_count": len(enriched), "failed_count": len(failed), "enriched": enriched, "failed": failed})
if __name__ == "__main__":
app.run(port=5001)
5. Run the application
python app.py
curl -X POST http://localhost:5001/enrich-contacts \
-H "Content-Type: application/json" \
-d '{"contacts": [{"linkedin_url": "https://www.linkedin.com/in/example"}]}'
6. Verify the result
{
"enriched_count": 1,
"failed_count": 0,
"enriched": [
{ "name": "...", "title": "...", "org": "...", "email": null, "phone": null }
],
"failed": []
}
email/phone being null here is a confirmed, honest limitation of this specific routine's output — not a bug. Pair this example with Enrich Person and Find Contact Details (a separate, real Clay function) if verified work email/phone is required.
How it works
Waterfall enrichment in Clay is exposed as a routine (a managed function), not a standalone REST endpoint. POST /routines/{routine_id}/run submits up to 100 items in one call; GET /routines/run/{routine_run_id}/results is polled until the run status flips to complete. Provider ordering, retries, and confidence scoring all happen inside Clay's managed infrastructure — your integration code is reduced to submit, poll, and parse.
Common issues
KeyError or empty profile fields despite status: complete
Cause: reading the result under "Enrich Person" (the display name) instead of the real, lowercase result key "Enrich person".
Fix: read result["Enrich person"]. Always confirm the exact result key from one live run rather than assuming it matches the routine's display name.
Contact enriched with no email/phone even though the request succeeded
Cause: this routine's real output does not include those fields for every input type.
Fix: don't fabricate them. Report null and, if phone/email are required, chain Enrich Person and Find Contact Details or Work Email (see Next steps) for that specific data.
400 Invalid request parameter(s): items.0.id
Cause: omitting the per-item id correlation key on the submitted batch.
Fix: every item in items needs its own caller-supplied string id, used to map results back to inputs — it's not optional.
Next steps
- Get verified work email + mobile phone — see Get Verified Work Email and Mobile Phone with Clay's Enrich Person and Find Contact Details
- Source the contact list first — see Find People at a Company with Clay's Search API
- Score enriched contacts — see Score Leads on Firmographic Signals with Clay
Verification
verification:
status: verified
tested_at: "2026-08-14"
product_version: "public/v0"
command: "python app.py && curl -X POST http://localhost:5001/enrich-contacts -d '{...}'"
expected_result: "real enriched profile fields (name/title/org/url) for a real LinkedIn URL"