Agent recipe

Find contact emails for a list of domains

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.

Going from a bare domain to the right person's work email takes three moves: confirm which company the domain belongs to, find who holds the role you want, and look up that person's address. Each move is one capability below.

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. Confirm the company behind each domain

    Company enrichment · Company enrichment

    Enrich each domain to confirm the company's name, its size and what it does before you look for people.

    Example input
    {
      "domain": "notion.so"
    }

    No tool in the catalog serves this step right now. The rest of the recipe still runs; check back for this one.

  2. Find who holds the role

    Web search · Web search

    Search the web for the role you want at that company, for example its head of partnerships, and note the person's name.

    Example input
    {
      "query": "Notion head of partnerships"
    }

    No tool in the catalog serves this step right now. The rest of the recipe still runs; check back for this one.

  3. Look up the work email

    Find an email · Email finder & verification · 15 tools · from $0.001

    Find the business email address for that name at the company's domain.

    Example input
    {
      "domain": "notion.so",
      "first_name": "Jane",
      "last_name": "Doe"
    }
    Tools for “Look up the work email”
    ToolPriceSuccess (7 days)Median latencyRuns (30 days)Tier
    Check a bulk work-email lookup jobReturns the current status of a bulk person email enrichment job — once completed, includes the results_url to download the full results$0.001 / call———bridged
    Find a work email from a name and domainName + company domain → the person's verified work email, with confidence score and the sources it was seen on$0.0216 / call———bridged
    Find an article author's email from its URLArticle or blog-post URL → the byline author's name and work email$0.0216 / call———bridged

    See all 15 tools for this step

Run the whole recipe

curl
# Needs curl and jq. Step 3 is shown; the other steps work the same way.
TOOL_ID=$(curl -s https://api.orcarouter.ai/v1/tools/discover \
  -H "Authorization: Bearer $ORCAROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"q": "email finder", "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": "notion.so", "first_name": "Jane", "last_name": "Doe"}}'
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", "web search", "email finder"]
TASK = "Find the head of partnerships at notion.so and their work email address."


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
// 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", "web search", "email finder"];
const TASK = "Find the head of partnerships at notion.so and their work email address.";

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();
© 2026 OrcaRouter

For Providers

Run an inference platform? Get your models on OrcaRouter.

providers@orcarouter.ai

Join our community

Discordsupport@orcarouter.aiXLinkedInGitHubYouTube