> ## 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.

# Node.js quickstart for the Forager API

> Node.js API quickstart with fetch: 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 Node.js with the built-in `fetch` (Node 18 or newer): 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 there is nothing to install.

**1. Set your credentials**

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
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 `process.env`; keep the key out of source files, logs and anything that ships to a browser. 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`.

```javascript first-lookup.mjs theme={"theme":{"light":"github-light","dark":"github-dark"}}
const BASE = "https://api-v2.forager.ai/api";
const ACCOUNT = process.env.FORAGER_ACCOUNT_ID;
const HEADERS = { "X-API-KEY": process.env.FORAGER_API_KEY, "Content-Type": "application/json" };

const resp = await fetch(`${BASE}/${ACCOUNT}/datastorage/person_detail_lookup/`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({ linkedin_public_identifier: "jane-doe-example" }),
});
if (!resp.ok) throw new Error(`Forager returned ${resp.status}: ${await resp.text()}`);
const person = await resp.json();
console.log(person.id, person.full_name, person.headline);
```

Run it with `node first-lookup.mjs`. 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**

`resp.ok` is false for every error. The cases below need different handling, so wrap `fetch` 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. |

```javascript forager-client.mjs theme={"theme":{"light":"github-light","dark":"github-dark"}}
const BASE = "https://api-v2.forager.ai/api";
export const ACCOUNT = process.env.FORAGER_ACCOUNT_ID;
const HEADERS = { "X-API-KEY": process.env.FORAGER_API_KEY, "Content-Type": "application/json" };

export class ForagerError extends Error {
  constructor(kind, status, body) {
    super(`Forager ${kind} (HTTP ${status}): ${body}`);
    this.kind = kind;     // "auth" | "credits" | "rate_limited" | "http"
    this.status = status;
  }
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

/** Call the Forager API. `path` is everything after /api/, for example
 *  `${ACCOUNT}/datastorage/person_detail_lookup/`. Returns the Response. */
export async function forager(method, path, { json, retries = 3 } = {}) {
  let wait = 2000;
  for (let attempt = 0; attempt <= retries; attempt++) {
    const resp = await fetch(`${BASE}/${path}`, {
      method,
      headers: HEADERS,
      body: json === undefined ? undefined : JSON.stringify(json),
    });
    if (resp.status === 403) throw new ForagerError("auth", 403, await resp.text());
    // 402 is not documented by Forager yet; kept as a guard for an out-of-credits rejection.
    if (resp.status === 402) throw new ForagerError("credits", 402, await resp.text());
    if (resp.status === 429) {
      if (attempt === retries) throw new ForagerError("rate_limited", 429, await resp.text());
      const retryAfter = Number(resp.headers.get("retry-after"));
      await sleep(retryAfter > 0 ? retryAfter * 1000 : wait);
      wait *= 2;
      continue;
    }
    if (!resp.ok) throw new ForagerError("http", resp.status, await resp.text());
    return resp;
  }
}

/** Parse a JSON body, treating an empty 2xx body as an empty result. */
export async function body(resp) {
  const text = await resp.text();
  return text ? JSON.parse(text) : null;
}
```

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.

**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.

```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { ACCOUNT, forager, body } from "./forager-client.mjs";

const submitted = await body(
  await forager("POST", `v2/${ACCOUNT}/datastorage/person_contacts_lookup/work_emails/`, {
    json: { first_name: "Jane", last_name: "Doe", domain: "acme.com" },
  }),
);
const jobId = 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.

```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function waitForWorkEmail(jobId, maxWaitMs = 300_000) {
  const deadline = Date.now() + maxWaitMs;
  let job;
  while (Date.now() < deadline) {
    const resp = await forager("GET", `v2/${ACCOUNT}/datastorage/person_contacts_lookup/work_emails/${jobId}/`);
    job = await body(resp);
    if (job.status === "completed" || job.status === "failed") return job;
    const retryAfter = Number(resp.headers.get("retry-after"));
    await sleep(retryAfter > 0 ? retryAfter * 1000 : 5000);
  }
  throw new Error(`work email job ${jobId} still ${job?.status} after ${maxWaitMs} ms`);
}

const job = await waitForWorkEmail(jobId);
if (job.status === "failed") console.log("failed:", job.error_code);   // e.g. person_not_found
else if (job.result === "found") console.log("work email:", job.emails[0]);
else console.log("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

Stub `fetch` in your tests so they never reach `api-v2.forager.ai`. In Node 18 and later, `globalThis.fetch` can be replaced with a function that returns `new Response(JSON.stringify(body), { status, headers })`: return a `403` to check the auth error, a `429` then a `200` to check the retry, and a `completed` job body to check the polling loop.

## Next steps

> [card] [Nightly job postings sync](https://www.forager.ai/docs/samples/nightly-job-postings-sync.md)
>
> A complete Node.js 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] [Python quickstart](https://www.forager.ai/docs/quickstarts/python.md)
>
> The same walkthrough with `requests`.


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