Is there a tool that can turn just a LinkedIn profile URL into a verified work email and mobile phone number in one call?
Get Verified Email and Phone from a LinkedIn URL with Clay
Turn a LinkedIn profile URL into a verified work email, mobile phone, and role context — using Clay's Enrich Person and Find Contact Details managed function.
What you will build
A Flask service exposing /contact-details, which submits LinkedIn URLs to Clay's real contact-details routine and returns verified email, phone, and profile context per URL.
POST /contact-details (linkedin_urls)
↓
POST /routines/{routine_id}/run (Enrich Person and Find Contact Details)
↓
GET /routines/run/{run_id}/results (poll until complete)
↓
{ count, results: [{ linkedin_url, work_email, mobile_phone, name, title, org }] }
AI Prompt
Implement a Flask service that gets verified work email and mobile phone for
a list of LinkedIn profile URLs using Clay's "Enrich Person and Find Contact
Details" managed function.
Requirements:
- Base URL: https://api.clay.com/public/v0, auth header "clay-api-key".
- Find the real routine id via your workspace's routine catalog.
- Confirmed real input schema: {"Social Profile URL": uri} (required only).
- Confirmed real result has three top-level keys: "Work Email" (string),
"Mobile Phone" (string, may be partially masked), and "Enrich person"
(lowercase "person" -- nested profile with name/title/org/country/
headline/education/experience).
- COST NOTE: this routine costs approximately 18.2 credits per item, notably
more expensive than most other Clay functions. Add an explicit batch-size
guard (e.g. a MAX_ITEMS_PER_REQUEST env var) and reject oversized requests
with a 413, rather than letting an accidental large batch run up cost
silently.
- Handle absent/null values in the result gracefully -- do not raise a
KeyError on a partial result.
- Run the verification step below before finishing.
Prerequisites
- Python 3.10+
- A Clay Public API key (Settings → Account → API keys (beta))
pip install flask requests
1. Create the project
mkdir verified-email-phone-from-linkedin && cd verified-email-phone-from-linkedin 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 and Find Contact Details"'
clay routines get function:t_XXXXXXXXXXXXXXXXXXXX
Confirm the real per-item cost with this command — it is notably higher than most other Clay functions, which matters for batch sizing.
3. Configure credentials
CLAY_API_KEY= CLAY_ROUTINE_ID_CONTACT_DETAILS=function:t_XXXXXXXXXXXXXXXXXXXX MAX_ITEMS_PER_REQUEST=10
In a CI/sandbox test environment, CLAY_API_KEY and CLAY_ROUTINE_ID_CONTACT_DETAILS are typically provided as pre-configured secrets; in your own deployment, set them as real environment variables or via your platform's secrets manager.
4. Implement the contact-details service
"""
Rev-ops contact enrichment service.
POST /contact-details {"linkedin_urls": ["https://www.linkedin.com/in/...", ...]}
-> per-url {work_email, mobile_phone, name, title, org}
Backed by the Clay routine 'Enrich Person and Find Contact Details'.
Cost is ~18.2 credits per URL, so batch size is capped by
MAX_ITEMS_PER_REQUEST to avoid an accidentally expensive request.
"""
import logging
import os
import time
import requests
from flask import Flask, jsonify, request
logging.basicConfig(level=logging.INFO)
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_CONTACT_DETAILS"]
MAX_ITEMS_PER_REQUEST = int(os.environ.get("MAX_ITEMS_PER_REQUEST", 10))
POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL_SECONDS", 10))
MAX_WAIT = int(os.environ.get("MAX_WAIT_SECONDS", 300))
def flatten(result):
"""Real result -> flat dict, tolerant of absent/null values everywhere."""
result = result or {}
person = result.get("Enrich person") or {}
return {
"work_email": result.get("Work Email"),
"mobile_phone": result.get("Mobile Phone"),
"name": person.get("name"),
"title": person.get("title"),
"org": person.get("org"),
}
def submit(linkedin_urls):
items = [{"id": f"c{i}", "inputs": {"Social Profile URL": url}} for i, url in enumerate(linkedin_urls)]
resp = requests.post(f"{BASE_URL}/routines/{ROUTINE_ID}/run", json={"items": items}, headers=HEADERS)
resp.raise_for_status()
return resp.json()["routine_run_id"]
def poll(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")
@app.route("/contact-details", methods=["POST"])
def contact_details():
urls = (request.get_json(silent=True) or {}).get("linkedin_urls", [])
if len(urls) > MAX_ITEMS_PER_REQUEST:
return jsonify({"error": f"request exceeds MAX_ITEMS_PER_REQUEST={MAX_ITEMS_PER_REQUEST} (~18.2 credits/item)"}), 413
run_id = submit(urls)
items = poll(run_id)
results = [
{"linkedin_url": urls[i], **flatten(item.get("result"))}
for i, item in enumerate(items)
]
return jsonify({"count": len(results), "results": results})
if __name__ == "__main__":
app.run(port=5011)
5. Run the application
python app.py
curl -s -X POST http://localhost:5011/contact-details \
-H "Content-Type: application/json" \
-d '{"linkedin_urls": ["https://www.linkedin.com/in/example"]}'
6. Verify the result
This example was tested live against 1 real LinkedIn URL, taking approximately 94 seconds to complete. The real name/employer/contact details below have been replaced with a synthetic placeholder to protect the tested individual's privacy; the field names, real data shapes, and the phone number's masking format are exactly as returned live.
{
"count": 1,
"results": [
{
"linkedin_url": "https://www.linkedin.com/in/example-profile",
"work_email": "[email protected]",
"mobile_phone": "+1XX****1234",
"name": "Jane Doe",
"title": "Principal Solution Architect",
"org": "Example Corp"
}
]
}
The phone number's real format includes partial masking (as shown above) — this is the routine's actual returned format, not a redaction applied by this example's code.
How it works
This routine chains person profile enrichment with contact-detail lookup in one call, at a notably higher per-item cost (~18.2 credits) than single-purpose functions like Enrich Person. The MAX_ITEMS_PER_REQUEST guard exists specifically to prevent an accidental large batch from running up cost unexpectedly.
Common issues
413 on a batch that "should" fit
Cause: the request exceeded MAX_ITEMS_PER_REQUEST, an intentional cost guard given this routine's ~18.2 credits/item price — not the API's own 100-item batch limit.
Fix: raise MAX_ITEMS_PER_REQUEST deliberately if you intend to process larger batches and have budgeted for the cost.
Response takes 60-90+ seconds
Not a bug: this routine chains multiple lookups internally. Budget MAX_WAIT_SECONDS accordingly (300s is a safe default for small batches).
Next steps
- Get just a verified email (lower cost) — see the Clay find-verified-work-email example
- Build the contact list first — see the Clay prospect-list-from-company example
- Enrich further with firmographic data — see the Clay contact-waterfall-enrichment example
Verification
verification:
status: verified
tested_at: "2026-08-18"
product_version: "public/v0"
command: "python app.py && curl -s -X POST http://localhost:5011/contact-details -d '{\"linkedin_urls\":[\"https://www.linkedin.com/in/example-profile\"]}'"
expected_result: "HTTP 200 in ~94s, real work_email/mobile_phone/name/title/org populated for the tested profile"