What a company enrichment API does
A company enrichment API takes something you already have — a domain, a work email, a URL — and returns what is known about the company behind it: name, industry, headcount band, founding year, location, company phone, LinkedIn page and a short description. It is the step that turns “jane@acme-logistics.com signed up” into “a 200-person freight company in Dallas signed up”, before the lead reaches a human or an agent.
This guide wires it up two ways against the BusinessMCP Data API: as a REST endpoint your code calls, and as an MCP tool an AI agent calls on its own. Both read the same company store — about 34M companies compiled from public registries, open company datasets, public business listings and company websites, refreshed by our own crawler — and both are billed the same way.
| REST | MCP | |
|---|---|---|
| Who calls it | Your code, at a moment you choose | An AI agent, when it judges the tool useful |
| Typical trigger | Signup webhook, CRM sync, nightly batch | A question in Claude, Cursor or your own agent |
| Endpoint | /api/hub/v1/companies/{domain} | https://businessmcp.com/api/hub/mcp |
| Discovery | OpenAPI document at /api/hub/v1/openapi.json | The client lists tools at connect time |
| Best at | Deterministic pipelines, caching, bulk | Ad-hoc research, multi-step reasoning |
Most teams end up with both: a backend hook that enriches every signup, and an agent that can look a company up mid-conversation. For the general trade-off between the two interfaces, see MCP vs API.
A working REST call
Create a key (they start with mcph_) in the developer portal — sign up here — and export it as BUSINESSMCP_API_KEY. Save a card on the Billing page (it is not charged) for a one-time $5 free for 7 days on first-party tools, so this costs nothing to try:
curl https://businessmcp.com/api/hub/v1/companies/stripe.com \
-H "Authorization: Bearer $BUSINESSMCP_API_KEY"The response is the company plus a meta envelope saying what the call cost:
{
"ok": true,
"data": {
"found": true,
"company": {
"domain": "stripe.com",
"name": "Stripe",
"industry": "financial services",
"category": null,
"employee_range": "5001-10000",
"employee_count": null,
"founded_year": 2010,
"country": "US",
"region": "california",
"city": "south san francisco",
"phone": null,
"linkedin_url": "https://www.linkedin.com/company/stripe",
"description": "Financial infrastructure for the internet.",
"tech_stack": null,
"brand_only": false,
"website_status": "live",
"source": "store"
}
},
"meta": {
"request_id": "…",
"tool": "company_lookup",
"provider": "first_party",
"trust_tier": "first_party",
"cost_usd": "$0.02",
"trial_usd": "$0.02",
"paid_usd": "$0",
"replayed": false
}
}Three things worth wiring from day one. found is the only field to branch on — a miss returns found: false and company: null, never an empty shell. The x-cost-usd response header (and meta.cost_usd) tells you what the call cost, and x-balance-usd what is left, so you can meter your own usage. And an Idempotency-Key request header becomes the call’s request id: send the same key again and you get the stored answer back with replayed: true instead of a second charge.
Give an AI agent the same lookup over MCP
The same tools are served as an MCP server, so an agent in Claude Desktop, Cursor, Claude Code or VS Code can call company_lookup, company_bulk_lookup, company_search and the rest with no glue code. Claude Desktop and Cursor read stdio configs, so they reach the remote endpoint through mcp-remote with the key as a header:
{
"mcpServers": {
"businessmcp": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://businessmcp.com/api/hub/mcp",
"--header",
"Authorization: Bearer mcph_YOUR_KEY_HERE"
]
}
}
}Claude Code takes the URL and header directly:
claude mcp add --transport http businessmcp https://businessmcp.com/api/hub/mcp \
--header "Authorization: Bearer mcph_YOUR_KEY_HERE"Restart the client and ask something only a lookup can answer: “What does the company behind acme-logistics.com do, and how big is it?” The agent picks company_lookup from its description, which states that it is charged only when a company is found. The server also carries hub_search_tools and hub_call_tool, so an agent can find a tool it was not told about.
Pricing: pay per company found
Every tool has a fixed price in dollars, paid from a prepaid balance: packs start at $10 and bigger ones add up to 30% bonus balance, after the one-time $5 free for 7 days trial. There is no subscription. The company tools only charge when they return something:
| Tool | Route | Charged |
|---|---|---|
| company_lookup | GET /companies/{domain} | $0.02 when a company is found |
| company_bulk_lookup | POST /companies/bulk (up to 100 domains) | $0.02 per company found; misses refunded |
| company_search | POST /companies/search (up to 25 results) | $0.01 per company returned |
| company_website_status | GET /companies/{domain}/status | $0.005 when we hold the domain |
| email_pattern | GET /companies/{domain}/email-pattern | $0.01 when a pattern is known |
Bulk calls reserve before they refund. A 100-domain bulk call reserves $2, runs, and refunds every domain that missed. That matters near an empty balance: a call that would cost $0.80 once the misses are refunded is still refused if $2 is not there. Size batches to your remaining balance.
Rate limits are 600 requests a minute per key and 3,000 per workspace, with a tighter 60 a minute for bulk and other per-unit tools — so bulk is the fast path: 60 calls of 100 domains is 6,000 domains a minute. Because the account is prepaid, a runaway loop can never spend more than the balance you put in, and you can give any key a monthly spend limit of its own.
curl -X POST https://businessmcp.com/api/hub/v1/companies/bulk \
-H "Authorization: Bearer $BUSINESSMCP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domains":["stripe.com","notarealcompany-xyz.com"]}'Caching: what to store and for how long
Firmographics change slowly, so the cheapest enrichment is the one you do not repeat. A sensible default:
- Filter first. gmail.com, outlook.com and other personal mail domains are not companies. Drop them before the call rather than paying to learn that.
- Key the cache on the normalised domain (lowercase, no www., no path) — the API normalises a URL or a work email the same way, so https://www.Acme.com/about and jane@acme.com are one entry.
- Cache hits for weeks. Name, industry and size rarely move month to month. Thirty days is a reasonable ceiling for a CRM field.
- Cache misses for days, not forever. The store grows as the crawler finds new companies, so a domain that missed this week may resolve next month.
- Treat website status as dated. It carries a checked_at timestamp from our crawler; re-ask when you are about to spend a sequence on the account.
// Enrich a signup domain: skip personal mail, cache hits and misses, never invent a company.
const BASE = "https://businessmcp.com/api/hub/v1"
const PERSONAL = new Set(["gmail.com", "outlook.com", "yahoo.com", "icloud.com", "proton.me"])
type Cached = { company: unknown | null; at: number }
const cache = new Map<string, Cached>() // use Redis or a table in production
const HIT_TTL = 30 * 86_400_000 // firmographics change slowly
const MISS_TTL = 7 * 86_400_000 // the store grows, so re-ask about misses
export async function enrich(emailOrDomain: string) {
const domain = emailOrDomain.split("@").pop()!.trim().toLowerCase()
if (PERSONAL.has(domain)) return null // not a company: do not pay for a lookup
const hit = cache.get(domain)
if (hit && Date.now() - hit.at < (hit.company ? HIT_TTL : MISS_TTL)) return hit.company
const res = await fetch(`${BASE}/companies/${encodeURIComponent(domain)}`, {
headers: {
Authorization: `Bearer ${process.env.BUSINESSMCP_API_KEY}`,
"Idempotency-Key": `enrich:${domain}:${new Date().toISOString().slice(0, 10)}`,
},
})
const body = await res.json()
if (!body.ok) {
// 402 = balance (or a key spend limit) used up, 429 = slow down, 503 = retry later.
if (body.error?.retryable) throw new Error(`retry later: ${body.error.code}`)
return null
}
const company = body.data.found ? body.data.company : null // found=false costs nothing
cache.set(domain, { company, at: Date.now() })
return company
}The pattern above never writes a guess into your CRM: a miss is stored as a miss, and a retryable error is raised rather than swallowed as “no company”.
What the data contains — and what it does not
Being precise about the edges is what keeps enrichment from quietly corrupting your records:
| Returned | Not returned |
|---|---|
| Company name, industry and category | People, named contacts or personal email addresses |
| Employee range and founding year | Revenue, funding or valuation |
| Country (ISO-2), region and city | Technographics — the tech_stack field is empty; do not build on it |
| Company phone and LinkedIn company page | Logos (we never hotlink a favicon service) |
| A short description (up to 400 characters) | Intent data or buying signals |
| Website status with the date it was measured | A guaranteed answer for every domain |
Brand platforms are returned name-only. A listing hosted on google.com or facebook.com is not Google or Facebook, so for those domains brand_only is true and phone and description are withheld rather than borrowed from a tenant page. Person-level data is a separate toolset with its own terms and geographic limits — see people data — and is never mixed into a company response.
Handling misses without making things up
Every enrichment source misses: new companies, holding domains, personal sites, parked domains. The failure to avoid is not the miss — it is turning the miss into a confident wrong answer.
For an agent, say so in its instructions: “If the lookup finds nothing, say the company is unknown. Do not infer one from the domain name.”
Errors come back as ok: false with a typed error.code and a retryable flag. Retry only when retryable is true (a 503 or a 429, which also sends retry-after: 60). A 402 means the balance (or that key’s spend limit) is used up and carries a link to add balance; a 400 means the input was not a domain. None of these should ever be written into a record as “no company”.
Frequently asked questions
Should I use the REST API or the MCP server for enrichment?
Use REST when your own code decides when to enrich, such as a signup webhook or a nightly CRM sync, because it is deterministic, cacheable and supports bulk calls. Use MCP when an AI agent should decide on its own to look a company up. The key, the tools and the prices are the same for both.
Am I charged when a company is not found?
No. company_lookup, company_bulk_lookup, company_search, company_website_status and email_pattern are charged only for results. A bulk call reserves the price of every domain first and refunds the misses when it finishes.
Can I pass an email address instead of a domain?
Yes. The lookup accepts a domain, a URL or a work email and normalises all three to the registrable domain. Filter out personal mail providers such as gmail.com first, because they are not companies.
How long should I cache enrichment results?
Cache found companies for up to about thirty days and misses for about a week. Firmographics change slowly, while the store keeps growing, so a domain that misses today may resolve later. Re-check website status before any outreach, because it is a dated measurement.
Does the company data include contacts or technology stack?
No. The company tools return company-level firmographics only. Named people are a separate toolset with its own terms, and the API is not a technographics source.
Sources
BusinessMCP Team
Every guide is written from running BusinessMCP on its own platform — the match rates, reply rates, and deliverability lessons are from our own data, not recycled blog folklore. About BusinessMCP
Turn your business into one AI-ready MCP server
Connect your tools, install one tracking script, and expose your unified data to any AI agent through a single secure endpoint.
Get started free