> ## Documentation Index
> Fetch the complete documentation index at: https://www.forager.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Forager is a REST API for B2B people, company and job post data: contact lookups (phone numbers, personal emails, work emails), person, company and job post search, and bulk data feeds.
> Server: https://api-v2.forager.ai. Send the API key in the X-API-KEY header. Most paths include {account_id}. Read FORAGER_API_KEY and FORAGER_ACCOUNT_ID from the environment; never put the key in client-side code, logs or prompts.
> To add Forager to a project, follow https://www.forager.ai/docs/guides/agent-quickstart.md. To pick an endpoint, read https://www.forager.ai/docs/guides/choose-an-endpoint.md. For end-to-end builds, see https://www.forager.ai/docs/recipes/overview.md.
> Most requests spend credits. Do not guess costs; use https://www.forager.ai/docs/api-overview/credit-pricing.md and ask the user before spending credits.
> Use only endpoints and fields defined in https://www.forager.ai/docs/openapi.yaml. Forager has no MCP server for the API yet; the /mcp endpoint on this site searches the documentation only.

# Python quickstart for the Forager API

> Python API quickstart with requests: key from the environment, a person lookup, handling for 403, 429 and out-of-credits responses, and a work email job.

This quickstart calls the Forager REST API from Python with the `requests` library: one person lookup, a small client that handles the failure responses, and an asynchronous work email lookup that you submit, poll and read. No Forager package is needed, and none is published yet, so `requests` is the whole dependency list.

**1. Install requests and set your credentials**

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pip install requests
export FORAGER_API_KEY="your-api-key"
export FORAGER_ACCOUNT_ID="your-account-id"
```

Create the key on the [API](https://app.forager.ai/keys) page of the Forager Web App and copy the Account ID from [My Account](https://app.forager.ai/profile). The code reads both from the environment; never put the key in source files or logs. See [API key authentication](https://www.forager.ai/docs/api-overview/authentication.md).

**2. Run your first lookup**

A [person lookup](https://www.forager.ai/docs/openapi/people/datastorage_person_detail_lookup_create.md) costs 1 credit and takes a LinkedIn public identifier (the slug after `/in/`) or a Forager `person_id`.

```python first_lookup.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
import requests

BASE = "https://api-v2.forager.ai/api"
ACCOUNT = os.environ["FORAGER_ACCOUNT_ID"]
HEADERS = {"X-API-KEY": os.environ["FORAGER_API_KEY"]}

resp = requests.post(
    f"{BASE}/{ACCOUNT}/datastorage/person_detail_lookup/",
    headers=HEADERS,
    json={"linkedin_public_identifier": "jane-doe-example"},
    timeout=30,
)
resp.raise_for_status()
person = resp.json()
print(person["id"], person["full_name"], person["headline"])
```

The response is one person record. Trimmed, with fictional values:

```json Response (illustrative) theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "id": 48213,
  "full_name": "Jane Doe",
  "headline": "Head of Data at Acme Example",
  "linkedin_info": { "public_identifier": "jane-doe-example" },
  "roles": [
    { "role_title": "Head of Data", "is_current": true, "organization": { "name": "Acme Example", "domain": "acme.com" } }
  ]
}
```

Keep `id`: the phone and personal email lookups take it as `person_id`.

**3. Handle 403, 429 and running out of credits**

`raise_for_status()` turns every error into the same exception. The cases below need different handling, so wrap the call in a small client:

| Status | What happened | What to do |
| - | - | - |
| `403` | `{"detail": "Authentication credentials were not provided."}`: the header is missing or the key is wrong, or the key cannot use this Account ID. | Fix the credentials. Never retry in a loop. |
| `429` | Rate limit. Documented for the [autocomplete](https://www.forager.ai/docs/openapi/autocomplete) and [feedback](https://www.forager.ai/docs/openapi/feedback) endpoints; the single work email lookup is also limited for interactive use. | Wait, then retry with backoff. Use `Retry-After` when it is sent. |
| Out of credits | A bulk work email batch your balance cannot cover is rejected and nothing is charged. The status code and body are not documented yet. | Buy credits in the [Web App](https://app.forager.ai/subscription), then resubmit. |

```python forager_client.py theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
import time
import requests

BASE = "https://api-v2.forager.ai/api"
ACCOUNT = os.environ["FORAGER_ACCOUNT_ID"]
HEADERS = {"X-API-KEY": os.environ["FORAGER_API_KEY"]}


class ForagerAuthError(Exception):
    """403: the API key or Account ID is wrong. Do not retry."""


class ForagerCreditsError(Exception):
    """The account has no credits left for this request."""


class ForagerRateLimited(Exception):
    """429 after every retry."""


def forager(method, path, *, json=None, params=None, retries=3):
    """Call the Forager API. `path` is everything after /api/, for example
    f"{ACCOUNT}/datastorage/person_detail_lookup/"."""
    wait = 2.0
    for attempt in range(retries + 1):
        resp = requests.request(method, f"{BASE}/{path}", headers=HEADERS,
                                json=json, params=params, timeout=30)
        if resp.status_code == 403:
            raise ForagerAuthError(resp.text)
        if resp.status_code == 402:
            # Not documented by Forager yet; kept as a guard for an out-of-credits rejection.
            raise ForagerCreditsError(resp.text)
        if resp.status_code == 429 and attempt < retries:
            time.sleep(float(resp.headers.get("Retry-After", wait)))
            wait *= 2
            continue
        if resp.status_code == 429:
            raise ForagerRateLimited(resp.text)
        resp.raise_for_status()
        return resp
    raise AssertionError("unreachable")


if __name__ == "__main__":
    person = forager("POST", f"{ACCOUNT}/datastorage/person_detail_lookup/",
                     json={"linkedin_public_identifier": "jane-doe-example"}).json()
    print(person["full_name"])
```

An empty list on a contact lookup is a result, not an error: the person was found but no number or address is on record. `requests` raises on a `2xx` with an empty body only when you call `.json()`, so check `resp.content` first for those endpoints.

**4. Submit an async work email lookup**

The [work email lookup](https://www.forager.ai/docs/openapi/work-emails/v2_datastorage_person_contacts_lookup_work_emails_create.md) answers `202` at once with a `job_id`, because verifying an address takes longer than a request should wait. Identify the person by name and company domain, or by LinkedIn public identifier.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from forager_client import ACCOUNT, forager

submitted = forager(
    "POST",
    f"v2/{ACCOUNT}/datastorage/person_contacts_lookup/work_emails/",
    json={"first_name": "Jane", "last_name": "Doe", "domain": "acme.com"},
).json()
job_id = submitted["job_id"]
```

```json 202 response (illustrative) theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "job_id": "2b7e1a40-3c5d-4f6e-8a9b-0c1d2e3f4a5b",
  "status": "queued",
  "status_check_url": "https://api-v2.forager.ai/api/v2/123456/datastorage/person_contacts_lookup/work_emails/2b7e1a40-3c5d-4f6e-8a9b-0c1d2e3f4a5b/",
  "webhook_url": ""
}
```

**5. Poll until the job is terminal**

Poll the [job result](https://www.forager.ai/docs/openapi/work-emails/v2_datastorage_person_contacts_lookup_work_emails_retrieve.md) until `status` is `completed` or `failed`. Polling is free. While the job is still running the response carries a `Retry-After` header that says how long to wait.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import time

def wait_for_work_email(job_id, max_wait=300):
    deadline = time.monotonic() + max_wait
    while time.monotonic() < deadline:
        resp = forager("GET", f"v2/{ACCOUNT}/datastorage/person_contacts_lookup/work_emails/{job_id}/")
        job = resp.json()
        if job["status"] in ("completed", "failed"):
            return job
        time.sleep(float(resp.headers.get("Retry-After", 5)))
    raise TimeoutError(f"work email job {job_id} still {job['status']} after {max_wait}s")

job = wait_for_work_email(job_id)
if job["status"] == "failed":
    print("failed:", job["error_code"])        # e.g. person_not_found, no_current_employer
elif job["result"] == "found":
    print("work email:", job["emails"][0])     # shortest confirmed address first
else:
    print("no work email on record")
```

```json Completed job (illustrative) theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "job_id": "2b7e1a40-3c5d-4f6e-8a9b-0c1d2e3f4a5b",
  "status": "completed",
  "result": "found",
  "emails": ["jane.doe@acme.com"],
  "error_code": null,
  "person_id": 48213
}
```

5 credits are charged only when `result` is `found`. To skip polling, send a `webhook_url` with the submit request and Forager POSTs the result there. For more than a handful of people, use the [bulk lookup](https://www.forager.ai/docs/guides/bulk-work-emails.md) instead of a loop of single lookups.

## Test without spending credits

Mock the HTTP layer in your tests so they never reach `api-v2.forager.ai`. With `requests`, the [responses](https://github.com/getsentry/responses) library or `unittest.mock.patch("requests.request")` is enough: return a `403` to check `ForagerAuthError`, a `429` then a `200` to check the retry, and a `completed` job body to check the polling loop.

## Next steps

> [card] [Enrich a CSV of LinkedIn URLs](https://www.forager.ai/docs/samples/enrich-csv-linkedin-urls.md)
>
> A complete Python script built on this client.

> [card] [Choose the right endpoint](https://www.forager.ai/docs/guides/choose-an-endpoint.md)
>
> Match what you have to the endpoint you need, with each call's credit cost.

> [card] [Errors and rate limits](https://www.forager.ai/docs/errors-and-rate-limits.md)
>
> Every documented status code and limit.

> [card] [Node.js quickstart](https://www.forager.ai/docs/quickstarts/node.md)
>
> The same walkthrough with `fetch`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.