{
  "version": 1,
  "slug": "find-phone-numbers-linkedin-python-api",
  "title": "How to Find Phone Numbers in Python Using a Waterfall Phone Finder API",
  "excerpt": "Find phone numbers from LinkedIn profile URLs in Python, then turn useful Waterfall Phone Finder results into a reviewed sales call list.",
  "cover": {
    "src": "/howto_images/find-phone-numbers-linkedin-python-api/find-phone-numbers-linkedin-python-api-cover.png",
    "optimized": "https://www.datablist.com/_next/image?url=%2Fhowto_images%2Ffind-phone-numbers-linkedin-python-api%2Ffind-phone-numbers-linkedin-python-api-cover.png&w=1200&q=75"
  },
  "url": "https://www.datablist.com/how-to/find-phone-numbers-linkedin-python-api",
  "contentMarkdown": "\nYou can find phone numbers from LinkedIn in Python by sending public\n`linkedin.com/in/...` profile URLs to Datablist's Waterfall Phone Finder API.\nThe API checks eligible phone-data providers in sequence and returns found\nnumbers in international format with their country codes.\n\nThis guide uses the Requests library to run one complete Python integration.\nThen it reuses the same results to prepare a local sales call list that keeps\neach prospect's name, LinkedIn URL, and account owner.\n\n> 📌 **Summary For Those In a Rush**\n>\n> Use one API to find phone numbers from a list of public LinkedIn profile URLs,\n> then prepare a local sales call list with each prospect's name and account\n> owner. The Waterfall Phone Finder tries eligible providers in sequence and\n> keeps found numbers ready for your team to review.\n>\n> - **Input:** Prospect records with a public LinkedIn profile URL.\n> - **Output:** A local call list with found international phone numbers,\n>   number-country codes, names, and account owners.\n\n> 💡 **Prefer a no-code solution?** Use Datablist's [Waterfall Phone\n> Finder](/enrichments/waterfall-phone-finder) to find phone numbers from\n> LinkedIn profile URLs without writing code.\n\nIf you start with names instead of profile URLs, follow the [no-code\nname-to-phone workflow](/how-to/find-phone-number-from-name) or [find LinkedIn\nprofiles from names in Python](/how-to/find-linkedin-profiles-python-api) first.\n\n## Set up Python, Requests, and your Datablist API key {#set-up-python-requests-and-your-datablist-api-key}\nFor this Python example, I use the `requests` package. Create a project folder,\nactivate a virtual environment, install Requests, and create the script file:\n\n```shell\nmkdir waterfall-phone-lookup\ncd waterfall-phone-lookup\npython -m venv .venv\nsource .venv/bin/activate\npython -m pip install requests\ntouch find_phone_numbers.py\nexport DATABLIST_API_KEY=\"your-api-key\"\n```\n\nCreate a [Personal API Key](https://app.datablist.com/account/api) in your\nDatablist account and use it for `DATABLIST_API_KEY`. REST API access is a\nGrowth-plan feature. I cover the credits used by successful lookups later in\nthe guide.\n\n## Find phone numbers with one complete Python function {#find-phone-numbers-with-one-complete-python-function}\nThe example uses three editable prospect records. Each record has a stable\n`prospect_id`, a name, a public LinkedIn URL, and the account owner responsible\nfor reviewing the result:\n\n- Florian Poullin at `https://www.linkedin.com/in/florianpoullin/`\n- Satya Nadella at `https://www.linkedin.com/in/satyanadella/`\n- Reid Hoffman at `https://www.linkedin.com/in/reidhoffman/`\n\nOnly the stable ID and LinkedIn URL need to go to the API. The name and account\nowner stay in your local source record. This separation keeps the lookup request\nsmall while preserving the business details your team needs after the result is\nreturned.\n\nPaste this complete script into `find_phone_numbers.py`:\n\n```python\nimport json\nimport os\nimport time\nimport uuid\n\nimport requests\n\n\ndef find_phone_numbers(prospects, api_key, settings):\n    \"\"\"Submit one phone lookup task, wait, 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\": prospect[\"prospect_id\"],\n            \"data\": {\"profileUrl\": prospect[\"linkedin_url\"]},\n        }\n        for prospect in prospects\n    ]\n\n    unique_id = str(uuid.uuid4())\n    print(f\"Task uniqueId: {unique_id}\", flush=True)\n\n    payload = {\n        \"enrichmentId\": \"waterfall-phone-finder\",\n        \"uniqueId\": unique_id,\n        \"settings\": settings,\n        \"inputs\": inputs,\n    }\n    response = requests.post(tasks_url, headers=headers, json=payload)\n    response.raise_for_status()\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(\n            f\"Task {task_id} {task['status']}: {task.get('error')}\"\n        )\n\n    results = []\n    results_url = f\"{task_url}/results\"\n    params = {\"limit\": 100}\n\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    api_key = os.environ.get(\"DATABLIST_API_KEY\")\n    if not api_key:\n        raise SystemExit(\"Set DATABLIST_API_KEY before running this script.\")\n\n    prospects = [\n        {\n            \"prospect_id\": \"prospect-florian-poullin\",\n            \"name\": \"Florian Poullin\",\n            \"linkedin_url\": \"https://www.linkedin.com/in/florianpoullin/\",\n            \"account_owner\": \"Habib\",\n        },\n        {\n            \"prospect_id\": \"prospect-satya-nadella\",\n            \"name\": \"Satya Nadella\",\n            \"linkedin_url\": \"https://www.linkedin.com/in/satyanadella/\",\n            \"account_owner\": \"Florian\",\n        },\n        {\n            \"prospect_id\": \"prospect-reid-hoffman\",\n            \"name\": \"Reid Hoffman\",\n            \"linkedin_url\": \"https://www.linkedin.com/in/reidhoffman/\",\n            \"account_owner\": \"Habib\",\n        },\n    ]\n    settings = {\"cacheAge\": \"30\"}\n\n    phone_results = find_phone_numbers(prospects, api_key, settings)\n    print(json.dumps(phone_results, indent=2, ensure_ascii=False))\n```\n\nRun the file with:\n\n```shell\npython find_phone_numbers.py\n```\n\nYou can also download Datablist's maintained [synchronous Requests\nexample](https://github.com/datablist/waterfall-phone-finder-api/blob/main/python/sync_requests.py).\nThe script above keeps the same simple order: submit one task, wait for it, and\nretrieve all result pages.\n\n### Submit the phone lookup {#submit-the-phone-lookup}\nThe list comprehension turns each source prospect into the API's input shape.\nYour `prospect_id` becomes `inputId`, while `linkedin_url` becomes\n`data.profileUrl`. Use a public person-profile URL with the\n`linkedin.com/in/...` path. Sales Navigator URLs are not supported by this\nenrichment.\n\nThe function generates `uniqueId` and prints it before the POST. The request\nsends that value with the `waterfall-phone-finder` enrichment ID, the cache\nsetting, and the inputs. A successful submission returns HTTP `202 Accepted`\nand a task `@id` while processing continues in the background.\n\nThe [Waterfall Phone Finder API\nreference](https://platform.datablist.com/docs/enrichments/tools/waterfall-phone-finder)\nlists the current settings, input fields, outputs, credits, and limits.\n\nFor one prospect, the request body follows this shape:\n\n```json\n{\n  \"enrichmentId\": \"waterfall-phone-finder\",\n  \"uniqueId\": \"generated-uuid\",\n  \"settings\": {\n    \"cacheAge\": \"30\"\n  },\n  \"inputs\": [\n    {\n      \"inputId\": \"prospect-florian-poullin\",\n      \"data\": {\n        \"profileUrl\": \"https://www.linkedin.com/in/florianpoullin/\"\n      }\n    }\n  ]\n}\n```\n\nThe public profile URL is the lookup input. `inputId` is the join key that\nconnects the result to the name and account owner in your source record. Choose\nan ID that is stable in your own system and unique within the task.\n\n### Wait for the background task {#wait-for-the-background-task}\nThe Requests client blocks locally while it checks the task every three\nseconds. The Datablist task still runs in the background. When its status becomes\n`completed`, the function moves to result retrieval. A `failed` or `stopped`\nstatus raises an error with the task ID so you can inspect it.\n\n### Retrieve every result page {#retrieve-every-result-page}\nThe results request asks for up to 100 rows at a time. It keeps fetching pages\nuntil `nextCursor` is `null`. Every result includes its source `inputId`, so you\ncan map it back to the correct prospect without relying on response order.\n\n## How the managed waterfall finds a phone number {#how-the-managed-waterfall-finds-a-phone-number}\nThe example sets `cacheAge` to `\"30\"`. Datablist first checks for an eligible\nresult no more than 30 days old. When one exists, the API can reuse it. The\nsetting accepts a cache hit but does not guarantee one.\n\nIf the profile remains unresolved, the managed waterfall tries eligible\nphone-data providers in sequence. A provider that returns no usable number lets\nthe record continue. A usable number stops the sequence for that record.\n\n![Waterfall phone lookup continuing after no result and stopping when a phone number is found](/howto_images/find-phone-numbers-linkedin-python-api/illustrations/waterfall-phone-lookup-sequence.png)\n\n### Why a waterfall helps across countries and markets {#why-a-waterfall-helps-across-countries-and-markets}\nDatablist does not rely on a single phone-data provider. Providers use different\ndatasets, and their coverage varies across countries and markets. When one\nprovider has no phone number, another can find it. Datablist manages these\nproviders through the same API integration.\n\nFor a sales team building a call list across several markets, this means fewer\nprospects are left out after a single provider returns no result. Your Python\ncode submits one task and reads one result format, so you do not need to build\nand maintain a separate integration for each provider.\n\nSet `cacheAge` to `\"0\"` when your workflow specifically needs to request a fresh\nlookup. Keep `\"30\"` for the example so an eligible cached result can be reused.\n\n## Read the complete result {#read-the-complete-result}\nHere is one result for Florian Poullin. Phone numbers are masked below.\n\n```json\n{\n  \"inputId\": \"prospect-florian-poullin\",\n  \"status\": \"success\",\n  \"outputs\": {\n    \"finderProvider\": \"wizleads\",\n    \"phoneNumber\": \"+33 • •• •• •• ••\",\n    \"phoneNumberCountry\": \"FR\"\n  },\n  \"creditsUsed\": \"500\",\n  \"error\": null\n}\n```\n\n`phoneNumber` contains the international number when one is available. The\n`+33` prefix and `phoneNumberCountry: \"FR\"` identify a French number.\n\n`finderProvider` is the finder reported for this result. The row used 500\ncredits, and `error: null` means the API returned no row-level error.\n\n## Prepare a reviewed sales call list {#prepare-a-reviewed-sales-call-list}\nOnce `phone_results` is available, you can prepare a local list for a sales team\nto review. Append this block after the result printing in the same file. It\nreuses the existing results and makes no second API call:\n\n```python\nresults_by_id = {\n    result[\"inputId\"]: result\n    for result in phone_results\n}\n\ncall_list = []\n\nfor prospect in prospects:\n    result = results_by_id.get(prospect[\"prospect_id\"])\n    if not result or result[\"status\"] != \"success\":\n        continue\n\n    phone_number = result.get(\"outputs\", {}).get(\"phoneNumber\")\n    if not phone_number:\n        continue\n\n    call_list_record = prospect.copy()\n    call_list_record[\"result\"] = result\n    call_list.append(call_list_record)\n\nprint(json.dumps(call_list, indent=2, ensure_ascii=False))\n```\n\nThe result index uses `inputId` as the join key, so each result stays tied to its\nsource record without depending on list position. The API payload contains each\nprospect's ID and profile URL, while the original `prospects` list keeps the name\nand account owner needed for the call list.\n\nFor Florian's row, `prospect-florian-poullin` connects the returned result to\nthe original `name`, `linkedin_url`, and `account_owner`. The team reads the\nphone and its `FR` country code from `result.outputs`. The complete API object,\nincluding status, credits, and error, remains attached under `result`.\n\nThe call-list loop follows the order of `prospects`. It copies each included\nprospect before attaching `result`, which leaves the original source list\navailable for corrections or a different follow-up channel.\n\nA no-result, error, or empty-phone row stays in the unchanged `prospects` list\nbut does not enter `call_list`. The code prepares a local sales call list for\nreview.\n\n## Estimate Waterfall Phone Finder API costs {#estimate-waterfall-phone-finder-api-costs}\nEach new uncached phone number found by the default managed waterfall uses 500\ncredits. An accepted cached result uses 0 credits, and a search without a result\nalso uses 0 credits. Read `creditsUsed` on each returned row for actual spend\ninstead of multiplying every submitted profile by 500.\n\nFor planning, use this formula:\n\n`new uncached phone numbers found × 500 credits`\n\nForty new numbers use 20,000 credits. One hundred new numbers use 50,000\ncredits. At the current base USD top-up rate, $20 buys 20,000 credits, so 500\ncredits are worth $0.50. The Growth subscription that provides REST API access\nis separate. See [Datablist pricing](/pricing) for current plans and credit\npackages.\n\nFor a planning example, suppose 100 profiles produce 20 new numbers, 5 accepted\ncached numbers, and 75 misses. The billable quantity is the 20 new numbers, so\nthe lookup uses 10,000 credits. The call list can contain all 25 found numbers.\nEnter `20` in the calculator, rather than all 100 inputs or all 25 found\nnumbers. This example illustrates the billing calculation, not an expected\nsuccess rate.\n\n<div format=\"cost-calculator\"\n     data-heading=\"Estimate your API cost\"\n     data-unit-label=\"New phone numbers found\"\n     data-credits-per-unit=\"500\"\n     data-lower-rate-label=\"New managed-waterfall result\"\n     data-initial-quantity=\"100\"\n     data-max-quantity=\"10000\">\n<p>Each new uncached phone number found uses 500 credits. Accepted cached results and 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}\n- **`DATABLIST_API_KEY` is missing:** the script stops before making a request.\n  Export the variable and run the file again.\n- **The API rejects the key:** `raise_for_status()` reports the HTTP error.\n  Copy the Personal API Key again and confirm that the request sends it in the\n  `X-API-Key` header.\n- **The profile URL is unsupported:** replace a blank, malformed, company, or\n  Sales Navigator URL with a public person profile using the\n  `linkedin.com/in/...` path.\n- **Credits are insufficient:** check the account balance and available credits\n  before submitting more work.\n- **The task is `failed` or `stopped`:** inspect its error, available results,\n  and credit use before deciding what still needs to run.\n- **A row has no result or an error:** keep the source prospect for correction\n  or another follow-up channel. The row alone does not explain which provider,\n  geography, or visibility condition caused the outcome.\n\nIf the POST response is lost, use the `uniqueId` printed before submission.\n**List your tasks before submitting again.** Replace `your-api-key` in this\nrequest, then find the task whose `uniqueId` matches the printed value:\n\n```shell\ncurl --get \"https://api.datablist.com/v1/enrichments/tasks\" \\\n  --header \"X-API-Key: your-api-key\" \\\n  --data-urlencode \"enrichmentId=waterfall-phone-finder\" \\\n  --data-urlencode \"limit=100\"\n```\n\nIf the task is not on the first page, follow the task-list response's\n`nextCursor`. Copy the matching task's `@id`, replace `TASK_ID` below, and check\nits status or retrieve its existing results:\n\n```shell\ncurl \"https://api.datablist.com/v1/enrichments/tasks/TASK_ID\" \\\n  --header \"X-API-Key: your-api-key\"\n\ncurl --get \"https://api.datablist.com/v1/enrichments/tasks/TASK_ID/results\" \\\n  --header \"X-API-Key: your-api-key\" \\\n  --data-urlencode \"limit=100\"\n```\n\nThe [task API documentation](https://platform.datablist.com/docs/enrichments/tasks)\nexplains task states, result pagination, and interruption behavior.\n\n## Next steps {#next-steps}\nYou now have one reusable Requests function for public LinkedIn profile URLs\nand one stable-ID merge that prepares found phone numbers for sales review.\n\nReplace the editable examples with a small set of qualified prospects. Keep\ntheir stable IDs and account owners, then review each found number before\npassing the prepared list to your team's calling process. Start with a small\nbatch so you can inspect the ID mapping and returned fields. If you need\nnon-blocking concurrency, use the maintained [async HTTPX\nexample](https://github.com/datablist/waterfall-phone-finder-api/blob/main/python/async_httpx.py).\n"
}