{
  "version": 1,
  "slug": "find-personal-emails-linkedin-python-api",
  "title": "How to Find Personal Emails in Python Using a Waterfall Personal Email Finder API",
  "excerpt": "Find personal emails from LinkedIn profile URLs in Python, then turn useful Waterfall Personal Email Finder results into recruiting contact records.",
  "cover": {
    "src": "/howto_images/find-personal-emails-linkedin-python-api/find-personal-emails-linkedin-python-api-cover.png",
    "optimized": "https://www.datablist.com/_next/image?url=%2Fhowto_images%2Ffind-personal-emails-linkedin-python-api%2Ffind-personal-emails-linkedin-python-api-cover.png&w=1200&q=75"
  },
  "url": "https://www.datablist.com/how-to/find-personal-emails-linkedin-python-api",
  "contentMarkdown": "\nYou can find personal emails from LinkedIn in Python by sending public `linkedin.com/in/...` profile URLs to Datablist's Waterfall Personal Email Finder API. The API runs a managed sequence of eligible email providers and returns a personal address when one of them finds a result.\n\nThe complete Requests example below submits one background task, waits for it to finish, and retrieves every result page. After the lookup works, you will reuse the returned objects to prepare recruiting contact records that keep each candidate's name, LinkedIn URL, and target role beside the found email.\n\nThis workflow looks for a personal email address that is independent of the candidate's current employer. If you need an address tied to the person's company instead, follow the separate guide to [find a work email from a LinkedIn profile](/how-to/find-email-from-linkedin-profile).\n\n> 🔑 **Summary For Those In a Rush**\n>\n> Supply candidate names, public LinkedIn profile URLs, and target roles. The Python workflow finds available personal emails and keeps each address with the recruiting details you started with.\n>\n> - **Input:** Candidate names, public `linkedin.com/in/...` profile URLs, and target roles.\n> - **Output:** Found personal emails kept with the matching candidate details.\n\n> 💡 **Prefer a no-code solution?** Use Datablist's [Waterfall Personal Email Finder](/enrichments/personal-email-finder-bulk) to find personal emails without writing code.\n\nIf your source contains names but no profile URLs, first [find public LinkedIn profiles in Python](/how-to/find-linkedin-profiles-python-api), then pass those URLs to this API.\n\n## Set up Python, Requests, and your Datablist API key {#set-up-python-requests-and-your-datablist-api-key}\nFor this example, I use Python with the `requests` package. Create a file named `find_personal_emails.py`, then install Requests:\n\n```shell\npython -m pip install requests\n```\n\nCreate a [Personal API Key](https://app.datablist.com/account/api) in your Datablist account and export it as an environment variable:\n\n```shell\nexport DATABLIST_API_KEY='your-api-key'\n```\n\nREST API access is available on the Growth plan. A successful personal-email result also consumes credits, which I calculate in the cost section below.\n\n## Find a personal email with one Python function {#find-a-personal-email-with-one-python-function}\nThe editable `candidates` list in this script contains one recruiting record. `target_role` is an example source value that stays local to the recruiting workflow. Only `candidate_id` and `linkedin_url` enter the API payload.\n\n```python\nimport json\nimport os\nimport time\nimport uuid\n\nimport requests\n\n\ndef find_personal_emails(candidates, api_key):\n    \"\"\"Submit one task, wait for it to finish, and return every result.\"\"\"\n    tasks_url = \"https://api.datablist.com/v1/enrichments/tasks\"\n    headers = {\"X-API-Key\": api_key}\n    inputs = [\n        {\n            \"inputId\": candidate[\"candidate_id\"],\n            \"data\": {\"profileUrl\": candidate[\"linkedin_url\"]},\n        }\n        for candidate in candidates\n    ]\n\n    unique_id = str(uuid.uuid4())\n    print(f\"Recovery uniqueId: {unique_id}\", flush=True)\n    payload = {\n        \"enrichmentId\": \"waterfall-personal-email-finder\",\n        \"uniqueId\": unique_id,\n        \"settings\": {},\n        \"inputs\": inputs,\n    }\n\n    response = requests.post(tasks_url, headers=headers, json=payload)\n    response.raise_for_status()\n\n    task = response.json()\n    task_id = task[\"@id\"]\n    task_url = f\"{tasks_url}/{task_id}\"\n\n    while task[\"status\"] not in (\"completed\", \"failed\", \"stopped\"):\n        time.sleep(3)\n        response = requests.get(task_url, headers=headers)\n        response.raise_for_status()\n        task = response.json()\n\n    if task[\"status\"] != \"completed\":\n        raise RuntimeError(f\"Task {task_id} {task['status']}: {task.get('error')}\")\n\n    results = []\n    results_url = f\"{task_url}/results\"\n    params = {\"limit\": 100}\n    while True:\n        response = requests.get(results_url, headers=headers, params=params)\n        response.raise_for_status()\n        page = response.json()\n        results.extend(page[\"results\"])\n\n        next_cursor = page[\"nextCursor\"]\n        if next_cursor is None:\n            break\n        params[\"cursor\"] = next_cursor\n\n    return results\n\n\nif __name__ == \"__main__\":\n    candidates = [\n        {\n            \"candidate_id\": \"candidate-florian-poullin\",\n            \"name\": \"Florian Poullin\",\n            \"linkedin_url\": \"https://www.linkedin.com/in/florianpoullin/\",\n            \"target_role\": \"Backend Engineer\",\n        }\n    ]\n\n    api_key = os.environ.get(\"DATABLIST_API_KEY\")\n    if not api_key:\n        raise RuntimeError(\"Set DATABLIST_API_KEY before running this script.\")\n\n    personal_email_results = find_personal_emails(candidates, api_key)\n    for result in personal_email_results:\n        print(json.dumps(result, indent=2))\n```\n\nRun the file from the same terminal:\n\n```shell\npython find_personal_emails.py\n```\n\nYou can also download the maintained [synchronous Requests example](https://github.com/datablist/waterfall-personal-email-finder-api/blob/main/python/sync_requests.py). The script above keeps the same submit, poll, and paginate sequence while using candidate IDs from the source records.\n\n### Submit the lookup {#submit-the-lookup}\nThe function turns each candidate into an API input. `inputId` receives the stable candidate ID, while `data.profileUrl` receives the public LinkedIn person-profile URL. A standard `linkedin.com/in/...` URL is required. Sales Navigator URLs, company pages, blank values, and names without profile URLs are not valid inputs for this enrichment.\n\nThe request uses `waterfall-personal-email-finder` as the `enrichmentId` and an empty `settings` object, as shown in the [Waterfall Personal Email Finder API documentation](https://platform.datablist.com/docs/enrichments/tools/waterfall-personal-email-finder). The script prints its generated `uniqueId` before submission. Keep that value because it lets you identify an existing task if the connection drops after the API accepts the request.\n\nHere is the complete request body for the one-profile example:\n\n```json\n{\n  \"enrichmentId\": \"waterfall-personal-email-finder\",\n  \"uniqueId\": \"55001d29-4fb9-4fe2-9497-80f4b346eed7\",\n  \"settings\": {},\n  \"inputs\": [\n    {\n      \"inputId\": \"candidate-florian-poullin\",\n      \"data\": {\n        \"profileUrl\": \"https://www.linkedin.com/in/florianpoullin/\"\n      }\n    }\n  ]\n}\n```\n\nA successful submission returns HTTP `202 Accepted` with the task ID in `@id`. Processing continues on Datablist's servers after that response.\n\n### Wait for the background task {#wait-for-the-background-task}\nThe Requests script blocks locally while the server runs the background task. It checks the task every three seconds and continues while the status is queued or running. A `completed` task moves to result retrieval. A `failed` or `stopped` task raises an error with the task ID and available error message.\n\n### Retrieve every result page {#retrieve-every-result-page}\nThe results endpoint returns each row's source ID, status, outputs, credits, and error. The first request asks for up to 100 results. **It fetches the next page until `nextCursor` is `null`.**\n\nThe task results preserve input order. The `inputId` still gives each result an explicit connection to its candidate record, including across multiple result pages.\n\nWhen you adapt the example for more records, keep each `candidate_id` stable from the source system. That value makes the join predictable when the task has multiple result pages.\n\n## How the personal-email waterfall finds a result {#how-the-personal-email-waterfall-finds-a-result}\nDatablist cleans the submitted public LinkedIn profile URL and passes the unresolved profile through currently eligible personal-email providers in sequence. If one provider returns no usable address, the profile can continue to the next eligible provider. When a provider finds a usable email, the sequence stops for that profile and returns it as `outputs.emailFound`.\n\nThe eligible providers are managed by Datablist, so your integration does not need provider-specific requests or fallback code. The public result reports the email outcome rather than a fixed provider order or attempt history.\n\n![Waterfall personal email lookup continuing after no result and stopping when an email is found](/howto_images/find-personal-emails-linkedin-python-api/illustrations/personal-email-waterfall-sequence.png)\n\nThe waterfall is the lookup method. Polling has a different job: it lets the Python client wait for that background work to finish.\n\n### Why a waterfall helps international recruiting teams {#why-a-waterfall-helps-international-recruiting-teams}\nPersonal-email providers use different datasets, so their strengths vary across countries and markets. When the first provider has no email for a candidate, another provider in the waterfall may find one.\n\nRecruiting teams working across several countries get broader provider coverage through one API integration. Datablist manages the sequence, so the team can recover contacts that a single-provider lookup might miss without connecting and maintaining several provider APIs.\n\n## Read the complete result {#read-the-complete-result}\n> 📘 **Masked email:** The local part of the address is replaced with `**` below. Your API result contains the full value.\n\nHere is the complete result page for the candidate input:\n\n```json\n{\n  \"taskId\": \"b5de73ffaa3f4f349c125d7c85125bb4\",\n  \"results\": [\n    {\n      \"inputId\": \"candidate-florian-poullin\",\n      \"status\": \"success\",\n      \"outputs\": {\n        \"emailFound\": \"**@yahoo.fr\"\n      },\n      \"creditsUsed\": \"75\",\n      \"error\": null\n    }\n  ],\n  \"nextCursor\": null\n}\n```\n\nThe `inputId` is the join key for the source candidate. `outputs.emailFound` contains the personal address. `creditsUsed` is the authoritative charge for this row, and `error: null` means the API returned no row-level error. The null cursor confirms that this response has no next result page.\n\nTreat the address as a contact option associated with the submitted profile and review it before using it in recruiting.\n\n## Prepare candidate contact records for recruiter review {#prepare-candidate-contact-records-for-recruiter-review}\nOnce the lookup returns, you can prepare a local list for recruiter review without another API request. The following block reuses the existing `candidates` and `personal_email_results` variables:\n\n```python\nresults_by_id = {\n    result[\"inputId\"]: result for result in personal_email_results\n}\nprepared_contacts = []\n\nfor candidate in candidates:\n    result = results_by_id.get(candidate[\"candidate_id\"])\n    if not result or result.get(\"status\") != \"success\":\n        continue\n\n    email_found = result.get(\"outputs\", {}).get(\"emailFound\")\n    if not email_found:\n        continue\n\n    prepared_contact = candidate.copy()\n    prepared_contact[\"result\"] = result\n    prepared_contacts.append(prepared_contact)\n\nprint(json.dumps(prepared_contacts, indent=2))\n```\n\nFor `candidate-florian-poullin`, the dictionary lookup finds the result with the same `inputId`. The copied record keeps Florian Poullin's name, LinkedIn URL, and example `Backend Engineer` target role. It then attaches the complete API object under `result`, where the recruiter can read `result.outputs.emailFound`.\n\nThe loop starts from the source candidate list. A future no-result, row error, missing result, or empty `emailFound` leaves that source list unchanged and does not enter `prepared_contacts`.\n\nFor a broader review process and other contact channels, read the guide to [finding a candidate's email address as a recruiter](/how-to/find-a-candidates-personal-email).\n\n## Estimate Waterfall Personal Email Finder API costs {#estimate-waterfall-personal-email-finder-api-costs}\nEach found personal email uses 75 credits. A lookup with no result uses 0 credits. For actual spend, read `creditsUsed` on each result instead of multiplying the total number of submitted profiles.\n\nThe planning formula is:\n\n`personal emails found × 75 credits`\n\nFor example, if you submit 100 profiles and 20 produce a personal email, the task uses `20 × 75 = 1,500 credits`. This is planning arithmetic, not an expected result rate. For larger budgets, 100 found emails use 7,500 credits and 1,000 found emails use 75,000 credits.\n\nAt the current base USD top-up rate, `$20 / 20,000 credits = $0.001 per credit`. One found email therefore consumes `$0.075` worth of credits, and 100 found emails consume `$7.50` worth. This estimate values the credits used. The Growth subscription required for REST API access is separate. Check [Datablist pricing](/pricing) for current plans and credit packages.\n\n<div format=\"cost-calculator\"\n     data-heading=\"Estimate your API cost\"\n     data-unit-label=\"Personal emails found\"\n     data-credits-per-unit=\"75\"\n     data-lower-rate-label=\"Successful personal-email result\"\n     data-initial-quantity=\"100\"\n     data-max-quantity=\"10000\">\n<p>Each found personal email uses 75 credits. Inputs with no result use 0 credits.</p>\n</div>\n\n## Fix common errors and recover an existing task {#fix-common-errors-and-recover-an-existing-task}\nMost problems happen before submission, at the HTTP request, or in an individual result. Use the error location to choose the next action:\n\n- **Missing `DATABLIST_API_KEY`:** the script stops locally before making a request. Export the environment variable, then run the file again.\n- **HTTP authentication failure:** check the Personal API Key in `DATABLIST_API_KEY`. The function sends it in the `X-API-Key` header. Correct the credential before submitting again.\n- **Unsupported profile URL:** replace a Sales Navigator URL, company page, blank value, or malformed URL with a public person profile in `linkedin.com/in/...` format.\n- **Insufficient credits:** check the account balance and available credits before submitting more work.\n- **Task is `failed` or `stopped`:** inspect the task error, available results, and credit usage before deciding what still needs to run.\n- **A row has no result or an error:** retain the source candidate for review or another contact channel. An unsuccessful lookup does not explain why an address was unavailable.\n\nIf the POST response is lost, list your tasks before creating another task. Search the response for the `uniqueId` printed by the script, then copy the matching task's `@id`:\n\n```shell\ncurl \"https://api.datablist.com/v1/enrichments/tasks?enrichmentId=waterfall-personal-email-finder&limit=100\" \\\n  --header \"X-API-Key: $DATABLIST_API_KEY\"\n\nTASK_ID='paste-matching-task-id'\n\ncurl \"https://api.datablist.com/v1/enrichments/tasks/$TASK_ID\" \\\n  --header \"X-API-Key: $DATABLIST_API_KEY\"\n\ncurl \"https://api.datablist.com/v1/enrichments/tasks/$TASK_ID/results\" \\\n  --header \"X-API-Key: $DATABLIST_API_KEY\"\n```\n\nIf the matching task is not in the first task-history response, follow the returned pagination cursor and check the next page. Continue checking the recovered task or retrieve its available results instead of automatically creating another paid task. The [task documentation](https://platform.datablist.com/docs/enrichments/tasks) covers the lifecycle, pagination, and result retention details.\n\n## Next steps {#next-steps}\nYou now have one reusable Python Requests function for public LinkedIn profile URLs and a stable-ID merge that prepares found personal-email results with each candidate's source context.\n\nReplace the example shortlist with a small set of your candidate records, keep a stable ID and target role on each one, then review every prepared contact before using it in your recruiting process. When your application needs non-blocking concurrency, use the maintained [async HTTPX example](https://github.com/datablist/waterfall-personal-email-finder-api/blob/main/python/async_httpx.py) as the next implementation step.\n"
}