groundtruth-company
Give it a company's website domain. Get back what is publicly known about the company, in one JSON object, with no scraping code and no dependencies.
pip install groundtruth-company
from groundtruth_company import Client
client = Client() # free tier, no key
info = client.company("stripe.com")
print(info["hiring"]["open_roles"], info["web"]["email_provider"])
Python 3.9+. Standard library only.
What one call returns
client.company(domain, name=None, country=None, ticker=None) runs several lookups in parallel and joins them:
| Part | What is in it |
|---|---|
web |
Domain record: registrar, created date, email provider, DNS, DMARC |
tech |
The website's tech stack: CMS, analytics, CDN, frameworks, payment processors, each with a confidence |
hiring |
From the company's own job board: open roles by department and place, salary ranges where published, roles opened and closed in the last 30 days |
registry |
The official company register entry. Add name and country (gb, no, fr, fi, cz, sk, ie, au, or global for LEI) |
sec |
Reported revenue from SEC filings. Add ticker (US public companies) |
missing |
Every part that had nothing to report, with the reason |
Each part stands alone: if one source is down or has nothing, the rest still answer and missing says why. Only a call where every part failed is an error, and that is never charged. Every item links to its source so you can check it.
{
"domain": "stripe.com",
"parts_found": ["web", "tech", "hiring"],
"web": {"registrar": "SafeNames Ltd.", "created": "1995-09-12", "email_provider": "Google Workspace", "dmarc": "reject"},
"tech": {"count": 21, "technologies": [{"name": "Cloudflare", "categories": ["CDN"], "confidence": 100}]},
"hiring": {"ats": "greenhouse", "board": "stripe", "open_roles": 412, "recent": {"window_days": 30, "opened": 37, "closed": 22}},
"missing": [{"part": "registry", "reason": "give name and country to add the official register entry"}]
}
(Trimmed to the shape. Real answers carry more fields.)
client.contacts(domain) reads the company's own website (home, contact, about, team and careers pages) and returns what the company published there:
- phone numbers
- role inboxes such as
sales@andinfo@ - the contact page and the postal address
- for US companies only, every other address at the company's own domain, exactly as published (people, departments, locations)
Every item carries the page it came from. Emails are never guessed from a pattern, addresses at other domains are left out, and robots.txt is honored. Results are cached for 30 days, so a site is read about once a month however often it is asked for.
{
"domain": "example-dental.com", "us_company": True,
"contact_page": "https://example-dental.com/contact",
"phones": [{"number": "+1 512-555-0142", "source_url": "https://example-dental.com/contact"}],
"role_emails": [{"email": "frontdesk@example-dental.com", "source_url": "https://example-dental.com/contact"}],
"other_emails": [{"email": "dr.lee@example-dental.com", "source_url": "https://example-dental.com/team"}],
"address": {"street": "100 Main St", "city": "Austin", "region": "TX", "postal_code": "78701", "country": "US", "source_url": "https://example-dental.com/"},
"pages_read": [{"url": "https://example-dental.com/", "kind": "home"}],
"notes": [], "fetched_at": "2026-10-30T18:00:00.000Z", "cached": False
}
Prices
| Free tier | 10 calls a day per caller, no key, no sign-up |
| $10 once | 2,000 calls that never expire. Buy |
| $49 a month | 20,000 calls a month, cancel any time. Buy |
| Pay per call, no account | x402 (USDC on Base) at /x402/v1/company and /x402/v1/company-contacts |
A company lookup uses 2 of a key's calls (so $10 is about 1,000 lookups, or $0.01 each). A contacts lookup uses 4 (about $0.02 each). Free-tier calls count as one each. A call that fails is not charged: a key gets its calls back.
The key is shown on the page Stripe sends you to after paying. Use it like this:
client = Client(api_key="your-key")
Live prices: https://companyfacts.dev/pricing
Errors
Every error is a subclass of GroundTruthError and carries .status and .body.
| Exception | HTTP | Meaning |
|---|---|---|
BadInputError |
400 | Missing or malformed input, for example not a domain. Not charged |
InvalidKeyError |
401 | The key is not active |
PaymentRequiredError |
402 (also 429) | Free allowance used up, or the key has no calls left. The message includes the buy links; also .buy_url and .monthly_url |
NotFoundError |
404 | Nothing found, for example an unregistered domain. Not charged |
SiteBlockedError |
422 | The company's site blocks automated readers. Not charged |
UpstreamError |
5xx, or no connection | An upstream source failed. Not charged, safe to retry |
from groundtruth_company import Client, PaymentRequiredError, NotFoundError
try:
info = Client().company("stripe.com")
except PaymentRequiredError as e:
print(e) # says what happened and where to buy a key
except NotFoundError:
print("no such company")
Examples
The tool functions below return the error message as text instead of raising, so an agent can read it and tell the user (including the buy link). Each example cites the framework docs it was written against.
Plain Python
import json
from groundtruth_company import Client
client = Client() # or Client(api_key="...")
company = client.company("stripe.com", name="Stripe")
print(json.dumps(company["hiring"], indent=2))
reach = client.contacts("example-dental.com")
for phone in reach["phones"]:
print(phone["number"], "from", phone["source_url"])
LangChain
A @tool wrapper:
# Docs: https://docs.langchain.com/oss/python/langchain/tools
import json
from langchain.agents import create_agent
from langchain.tools import tool
from groundtruth_company import Client, GroundTruthError
gt = Client() # or Client(api_key="...")
@tool
def company_lookup(domain: str) -> str:
"""Everything public about a company from its website domain: registrar, DNS and email
provider, tech stack, and hiring (open roles, salary ranges, recent openings and closings)."""
try:
return json.dumps(gt.company(domain))
except GroundTruthError as e:
return str(e)
@tool
def company_contacts(domain: str) -> str:
"""How to reach a company, from its own website: phones, role inboxes such as sales@,
postal address, and the contact page. Every item names its source page."""
try:
return json.dumps(gt.contacts(domain))
except GroundTruthError as e:
return str(e)
agent = create_agent(model="openai:gpt-5.5", tools=[company_lookup, company_contacts])
result = agent.invoke({"messages": [{"role": "user", "content": "Is stripe.com hiring? What do they run on?"}]})
The remote MCP endpoint (company_lookup is in it; company-contacts is REST and x402 only for now). Current LangChain moved MCP into langchain.mcp:
# Docs: https://docs.langchain.com/oss/python/langchain/mcp
# https://docs.langchain.com/oss/python/langchain/mcp/auth.md (bearer token via a FastMCP client)
# pip install "langchain[mcp]"
from fastmcp.client import Client
from langchain.agents import create_agent
from langchain.mcp import MCPAdapter
URL = "https://companyfacts.dev/mcp"
async def main():
# Free tier, no key:
async with MCPAdapter(URL) as adapter:
tools = await adapter.list_tools()
agent = create_agent("openai:gpt-5.5", tools)
return await agent.ainvoke({"messages": [{"role": "user", "content": "Look up stripe.com"}]})
# With a key, pass a FastMCP client that carries it:
# async with MCPAdapter(Client(URL, auth="your-key")) as adapter: ...
With the older langchain-mcp-adapters package (its repository is archived, and its README points to langchain.mcp above, but it still works):
# Docs: https://github.com/langchain-ai/langchain-mcp-adapters
# pip install langchain-mcp-adapters
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"groundtruth": {
"transport": "http",
"url": "https://companyfacts.dev/mcp",
# "headers": {"Authorization": "Bearer your-key"}, # optional, lifts the free tier
}
})
tools = await client.get_tools()
CrewAI
A custom BaseTool:
# Docs: https://docs.crewai.com/en/learn/create-custom-tools
import json
from typing import Type
from crewai import Agent, Crew, Task
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
from groundtruth_company import Client, GroundTruthError
gt = Client() # or Client(api_key="...")
class DomainInput(BaseModel):
domain: str = Field(..., description="The company's website domain, for example stripe.com")
class CompanyLookupTool(BaseTool):
name: str = "Company lookup"
description: str = (
"Everything public about a company from its website domain: registrar, DNS and email "
"provider, tech stack, and hiring (open roles, salary ranges, recent openings and closings)."
)
args_schema: Type[BaseModel] = DomainInput
def _run(self, domain: str) -> str:
try:
return json.dumps(gt.company(domain))
except GroundTruthError as e:
return str(e)
class CompanyContactsTool(BaseTool):
name: str = "Company contacts"
description: str = (
"How to reach a company, from its own website: phones, role inboxes such as sales@, "
"postal address and contact page. Every item names its source page."
)
args_schema: Type[BaseModel] = DomainInput
def _run(self, domain: str) -> str:
try:
return json.dumps(gt.contacts(domain))
except GroundTruthError as e:
return str(e)
researcher = Agent(
role="Company researcher",
goal="Find out what a company does, what it runs on, whether it is hiring, and how to reach it",
backstory="You check public facts and cite where they came from.",
tools=[CompanyLookupTool(), CompanyContactsTool()],
)
task = Task(
description="Look up stripe.com and summarise hiring and tech stack.",
expected_output="A short summary with the numbers.",
agent=researcher,
)
print(Crew(agents=[researcher], tasks=[task]).kickoff())
The remote MCP endpoint through CrewAI's MCPServerAdapter:
# Docs: https://docs.crewai.com/en/mcp/overview
# https://docs.crewai.com/en/mcp/streamable-http
# pip install 'crewai-tools[mcp]'
from crewai import Agent
from crewai_tools import MCPServerAdapter
server_params = {
"url": "https://companyfacts.dev/mcp",
"transport": "streamable-http",
}
with MCPServerAdapter(server_params, "company_lookup", connect_timeout=60) as tools:
researcher = Agent(
role="Company researcher",
goal="Look up companies by domain",
backstory="You check public facts.",
tools=tools,
)
With a key, CrewAI's documented way to send a header is the mcps field with MCPServerHTTP:
# Docs: https://docs.crewai.com/en/mcp/overview (structured configuration)
from crewai import Agent
from crewai.mcp import MCPServerHTTP
researcher = Agent(
role="Company researcher",
goal="Look up companies by domain",
backstory="You check public facts.",
mcps=[
MCPServerHTTP(
url="https://companyfacts.dev/mcp",
headers={"Authorization": "Bearer your-key"},
streamable=True,
),
],
)
LlamaIndex
A FunctionTool:
# Docs: https://developers.llamaindex.ai/python/framework/module_guides/deploying/agents/tools/
import json
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.core.tools import FunctionTool
from groundtruth_company import Client, GroundTruthError
gt = Client() # or Client(api_key="...")
def company_lookup(domain: str) -> str:
"""Everything public about a company from its website domain: registrar, DNS and email
provider, tech stack, and hiring (open roles, salary ranges, recent openings and closings)."""
try:
return json.dumps(gt.company(domain))
except GroundTruthError as e:
return str(e)
def company_contacts(domain: str) -> str:
"""How to reach a company, from its own website: phones, role inboxes such as sales@,
postal address and contact page. Every item names its source page."""
try:
return json.dumps(gt.contacts(domain))
except GroundTruthError as e:
return str(e)
tools = [FunctionTool.from_defaults(company_lookup), FunctionTool.from_defaults(company_contacts)]
agent = FunctionAgent(llm=llm, tools=tools) # llm: any LlamaIndex LLM you have configured
response = await agent.run("Is stripe.com hiring? What do they run on?")
The remote MCP endpoint with McpToolSpec:
# Docs: https://developers.llamaindex.ai/python/framework-api-reference/tools/mcp/
# pip install llama-index-tools-mcp
from llama_index.core.agent.workflow import FunctionAgent
from llama_index.tools.mcp import BasicMCPClient, McpToolSpec
client = BasicMCPClient(
"https://companyfacts.dev/mcp",
# headers={"Authorization": "Bearer your-key"}, # optional, lifts the free tier
)
tool_spec = McpToolSpec(client=client, allowed_tools=["company_lookup"])
tools = await tool_spec.to_tool_list_async()
agent = FunctionAgent(llm=llm, tools=tools)
How these examples were checked
Import paths were read from the framework docs linked above on 2026-10-30. The four MCP connection snippets without a key (langchain.mcp, langchain-mcp-adapters, CrewAI MCPServerAdapter, LlamaIndex McpToolSpec) were each run against the live endpoint and listed the company_lookup tool. The key variants (headers, auth=, MCPServerHTTP) follow the docs but were not run with a real key. The agent wiring (model names, llm) is yours to fill in.
Other things this API does
The same host serves job-board data (open jobs at thousands of companies), web page reading, email checks, domain records and more. The full list: https://companyfacts.dev/llms.txt
MIT licensed.
Metadata
Release files for groundtruth-company 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| groundtruth_company-0.1.1.tar.gz | 11.6 kB | Without provenance | Details for groundtruth_company-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| groundtruth_company-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for groundtruth_company-0.1.1-py3-none-any.whl |
Total release size: 22.4 kB
Release files / groundtruth_company-0.1.1.tar.gz
| Download URL | groundtruth_company-0.1.1.tar.gz |
|---|---|
| Size | 11.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
38a206b1fdbb897355bfcb54bec0f30f5fba330e479056a49b559712445528ff
|
|
BLAKE2b-256 checksum How to use checksums |
e0dcc4cfd6141a232dc486354e1301fa6e1e23994b0c1cb0fa0fd8f02caa8ecd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Release files / groundtruth_company-0.1.1-py3-none-any.whl
| Download URL | groundtruth_company-0.1.1-py3-none-any.whl |
|---|---|
| Size | 10.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d890660e1465abb79871b4468ae182f0f87a8c9a8c0c257e357511cc836deeba
|
|
BLAKE2b-256 checksum How to use checksums |
c1fd189f8064569452878dddf906d01b5010ba092b1ad82e3f7a10e587b9bba9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Security for groundtruth-company 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.