# Enrich a lead list

Turn a list of company domains into firmographics, a contact's work email and a recent news hook, with one API key.

A list of company domains is a start; a sales team also needs firmographics, a named contact and a reason to reach out. This recipe adds all three: company enrichment for size and industry, an email finder for the contact, and web search for a recent news hook.

Email finders return business addresses for people at a company domain. Use them for legitimate outreach and follow the rules that apply to you, such as GDPR and CAN-SPAM. Input fields can differ from one source to the next, so read a tool's schema with `GET /v1/tools/{id}` before you call it.

## Steps

### 1. Add firmographics for each domain

Look up each company by its domain to fill in its industry, size, location and links.

Platform: Company enrichment · Capability: Company enrichment

No tool serves this step right now.

Example input:

```json
{"domain":"stripe.com"}
```

### 2. Find a contact's work email

For the person you want to reach, look up a business email address at the company's domain.

Platform: [Email finder \& verification](https://www.orcarouter.ai/tools/email-finder.md) · Capability: [Find an email](https://www.orcarouter.ai/tools/email-finder/email-find.md) · 15 tools, from $0.001 per call

| Tool | Source | Price | Health |
| --- | --- | --- | --- |
| [Check a bulk work-email lookup job](https://www.orcarouter.ai/tools/email-finder/email-find/people-email-bulk-status.md) | — | $0.001 per call | OK |
| [Find a work email from a name and domain](https://www.orcarouter.ai/tools/email-finder/email-find/people-email-find-7bb6d8.md) | — | $0.02156 per call | OK |
| [Find an article author's email from its URL](https://www.orcarouter.ai/tools/email-finder/email-find/people-email-find-author.md) | — | $0.02156 per call | OK |

Example input:

```json
{"domain":"stripe.com","first_name":"Jane","last_name":"Doe"}
```

### 3. Add a recent news hook

Search the web for the company's latest news, so the first message can mention something current.

Platform: [Web search](https://www.orcarouter.ai/tools/web-search.md) · Capability: Web search

No tool serves this step right now.

Example input:

```json
{"query":"Stripe news this month"}
```

## Code

### curl

```shell
# Needs curl and jq. Step 1 is shown; repeat it with each step's query.
TOOL_ID=$(curl -s https://api.orcarouter.ai/v1/tools/discover \
  -H "Authorization: Bearer $ORCAROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q": "company enrichment", "limit": 1}' | jq -r '.data.results[0].id')

# The inputs the tool takes
curl -s "https://api.orcarouter.ai/v1/tools/$TOOL_ID" \
  -H "Authorization: Bearer $ORCAROUTER_API_KEY" | jq '.data.input_schema'

# Run it with those inputs
curl -s "https://api.orcarouter.ai/v1/tools/$TOOL_ID/run" \
  -H "Authorization: Bearer $ORCAROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input": {"domain": "stripe.com"}}'
```

### Python

```python
# pip install requests openai
import json
import os

import requests
from openai import OpenAI

BASE = "https://api.orcarouter.ai"
KEY = os.environ["ORCAROUTER_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}

# One discover query per step of the recipe.
STEPS = ["company enrichment", "email finder", "web search"]
TASK = (
    "For stripe.com and notion.so, give me each company's industry and size, "
    "the work email of its head of marketing if you can find one, "
    "and one recent news headline."
)


def discover(query, limit=2):
    r = requests.post(f"{BASE}/v1/tools/discover", json={"q": query, "limit": limit}, headers=HEADERS, timeout=30)
    r.raise_for_status()
    return [tool["id"] for tool in r.json()["data"]["results"]]


def run(name, tool_input):
    r = requests.post(f"{BASE}/v1/tools/{name}/run", json={"input": tool_input}, headers=HEADERS, timeout=120)
    return r.text  # the tool's answer, or its error, goes back to the model


ids = [tool_id for query in STEPS for tool_id in discover(query)]
fns = requests.get(
    f"{BASE}/v1/tools/functions",
    params={"ids": ",".join(ids), "format": "openai"},
    headers=HEADERS,
    timeout=30,
)
fns.raise_for_status()
tools = fns.json()["data"]["tools"]

llm = OpenAI(base_url=f"{BASE}/v1", api_key=KEY)
messages = [{"role": "user", "content": TASK}]
for _ in range(10):
    msg = llm.chat.completions.create(model="openai/gpt-5-mini", messages=messages, tools=tools).choices[0].message
    messages.append(msg)
    if not msg.tool_calls:
        print(msg.content)
        break
    for call in msg.tool_calls:
        result = run(call.function.name, json.loads(call.function.arguments or "{}"))
        messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
```

### TypeScript

```typescript
// npm install openai
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources/chat/completions";

const BASE = "https://api.orcarouter.ai";
const KEY = process.env.ORCAROUTER_API_KEY!;
const headers = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };

// One discover query per step of the recipe.
const STEPS = ["company enrichment", "email finder", "web search"];
const TASK =
  "For stripe.com and notion.so, give me each company's industry and size, " +
  "the work email of its head of marketing if you can find one, " +
  "and one recent news headline.";

async function discover(q: string, limit = 2): Promise<string[]> {
  const res = await fetch(`${BASE}/v1/tools/discover`, {
    method: "POST",
    headers,
    body: JSON.stringify({ q, limit }),
  });
  if (!res.ok) throw new Error(`discover: HTTP ${res.status}`);
  const { data } = await res.json();
  return data.results.map((tool: { id: string }) => tool.id);
}

async function run(name: string, input: unknown): Promise<string> {
  const res = await fetch(`${BASE}/v1/tools/${name}/run`, {
    method: "POST",
    headers,
    body: JSON.stringify({ input }),
  });
  return res.text(); // the tool's answer, or its error, goes back to the model
}

async function main() {
  const ids = (await Promise.all(STEPS.map((q) => discover(q)))).flat();
  const fns = await fetch(
    `${BASE}/v1/tools/functions?format=openai&ids=${encodeURIComponent(ids.join(","))}`,
    { headers },
  );
  if (!fns.ok) throw new Error(`functions: HTTP ${fns.status}`);
  const { data } = await fns.json();

  const llm = new OpenAI({ baseURL: `${BASE}/v1`, apiKey: KEY });
  const messages: ChatCompletionMessageParam[] = [{ role: "user", content: TASK }];
  for (let turn = 0; turn < 10; turn++) {
    const reply = await llm.chat.completions.create({ model: "openai/gpt-5-mini", messages, tools: data.tools });
    const msg = reply.choices[0].message;
    messages.push(msg);
    if (!msg.tool_calls?.length) {
      console.log(msg.content);
      return;
    }
    for (const call of msg.tool_calls) {
      const fn = call.type === "function" ? call.function : undefined;
      const result = fn ? await run(fn.name, JSON.parse(fn.arguments || "{}")) : "unsupported tool call";
      messages.push({ role: "tool", tool_call_id: call.id, content: result });
    }
  }
}

main();
```

## Related recipes

- [Monitor brand mentions on the web and TikTok](https://www.orcarouter.ai/tools/recipes/monitor-brand-mentions.md): Search the web and TikTok for your brand, then read the comments under the posts that mention it: a daily brand check an agent can run on its own.
- [Find contact emails for a list of domains](https://www.orcarouter.ai/tools/recipes/find-contact-emails-for-domains.md): Go from a bare domain to the right person's work email: confirm the company, find who holds the role and look up the address.
- [Brief yourself on a DeFi protocol](https://www.orcarouter.ai/tools/recipes/research-a-defi-protocol.md): Pull a protocol's total value locked and its latest news, so an agent can write a one-page brief with figures and sources.

---

Markdown version of https://www.orcarouter.ai/tools/recipes/enrich-a-lead-list
