clay.com

Command Palette

Search for a command to run...

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?

Last updated: 9/9/2026

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 clay CLI, or equivalent workspace access, to discover the real Enrich Person routine 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"

Related Articles