Enrich a lead from a domain
Turn a work email or a domain into a company record with headcount, headquarters and two views of its tech stack.
A lead signs up with jane@northwind.example. Three calls fill in the CRM record: who the company is, what it hires for, and what its website runs. A full function that combines them is at the end.
| Call | What it adds | Cost |
|---|---|---|
GET /v1/companies/{key} | Name, domain, logo, headcount, headquarters, description, founding year | 1 credit |
GET /v1/companies/{key}/technologies (beta) | Technologies the company's job postings name, graded by evidence | 1 credit |
POST /v1/stack/scan | Technologies detected on the company's live homepage | 1 credit |
The company tech stack endpoint is in beta, enabled per account. Without access it answers 404 with {"error": "company_technographics_not_enabled"} and charges nothing. Request access. The other two calls work on every plan.
1. Look up the company
The key can be a domain, a careers URL, an email address or a company name. An email or URL must be percent-encoded. A domain resolves more often than a name.
curl "https://api.jobspipe.dev/v1/companies/jane%40northwind.example" \
-H "Authorization: Bearer jp_live_your_key_here"{
"name": "Northwind Analytics",
"domain": "northwind.example",
"url": "https://northwind.example",
"logo": null,
"employee_count": 1800,
"description": "Northwind builds analytics software for retailers.",
"location": { "street": null, "city": "Berlin", "region": "Berlin", "postal_code": null, "country": "DE" },
"founded": "2009"
}Every field except name can be null. A 404 is a normal answer: no confident record matched, so treat it as "unknown" and move on. It still costs 1 credit.
2. What the company hires for
Job postings show the tools a company's teams use day to day: data warehouse, languages, CRM, ERP.
curl "https://api.jobspipe.dev/v1/companies/northwind.example/technologies?tier_min=likely&kind=product" \
-H "Authorization: Bearer jp_live_your_key_here"{
"as_of": "2026-09-25 06:00:00",
"company": { "id": "2512632719191904832", "name": "Northwind Analytics", "domain": "northwind.example" },
"num_jobs": 214,
"num_technologies": 2,
"data": [
{
"technology": { "slug": "snowflake", "name": "Snowflake", "category": "Data Warehouse", "kind": "product" },
"tier": "likely",
"confidence": "medium",
"jobs": 27,
"required_jobs": 19,
"last_date_found": "2026-09-22",
"recency": "active",
"share": 0.1262
},
{
"technology": { "slug": "salesforce", "name": "Salesforce", "category": "CRM", "kind": "product" },
"tier": "likely",
"confidence": "medium",
"jobs": 6,
"required_jobs": 4,
"last_date_found": "2026-09-10",
"recency": "active",
"share": 0.028
}
]
}share is the fraction of the company's postings that name the technology. A 404 here means either no technologies were found for the company or the beta is not enabled; the error body tells you which.
3. What the website runs
The stack scan detects what the company's homepage serves: frameworks, analytics, CDN, payment and chat widgets.
curl https://api.jobspipe.dev/v1/stack/scan \
-H "Authorization: Bearer jp_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "domain": "northwind.example" }'{
"domain": "northwind.example",
"scanned_at": "2026-09-20T08:14:00Z",
"http_status": 200,
"detected": [
{ "slug": "nextjs", "name": "Next.js", "categories": ["Web frameworks"], "confidence": 100, "version": null, "saas": false, "oss": true },
{ "slug": "hubspot", "name": "HubSpot", "categories": ["Marketing automation"], "confidence": 100, "version": null, "saas": true, "oss": false }
]
}http_status: 0 means the scanner could not reach the domain. scanned_at is when this stack was first seen unchanged, so read it as "the site has looked like this since".
Why use both stack sources
The two sources see different parts of a company:
| Company tech stack (job postings) | Stack scan (website) | |
|---|---|---|
| Sees | Internal tools: data platforms, languages, cloud, ERP, CRM, certifications | Public-facing tools: frameworks, analytics, CDN, marketing and payment widgets |
| Evidence | Tier, posting counts, required or mentioned, first and last date | Fingerprint matches with a 0-100 confidence |
| Strong for | "Do they run Snowflake?" "Are they hiring Kubernetes engineers?" | "Do they use HubSpot on their site?" "Which analytics do they load?" |
| Misses | Tools nobody hires for by name | Everything behind the login or inside the company |
Put it together
One CRM record from a work email. A missing piece becomes an empty value, so one unknown company does not stop a batch.
from urllib.parse import quote
import requests
API = "https://api.jobspipe.dev"
HEADERS = {"Authorization": "Bearer jp_live_your_key_here"}
def enrich(email: str) -> dict:
record = {"email": email}
r = requests.get(f"{API}/v1/companies/{quote(email, safe='')}", headers=HEADERS, timeout=30)
if r.status_code != 200:
return record
company = r.json()
record.update(
company=company["name"],
domain=company["domain"],
employees=company["employee_count"],
country=(company["location"] or {}).get("country"),
)
domain = company["domain"] or email.split("@", 1)[1]
r = requests.get(
f"{API}/v1/companies/{domain}/technologies",
headers=HEADERS,
params={"tier_min": "likely"},
timeout=30,
)
record["hires_for"] = [t["technology"]["slug"] for t in r.json()["data"]] if r.status_code == 200 else []
r = requests.post(f"{API}/v1/stack/scan", headers=HEADERS, json={"domain": domain}, timeout=60)
record["website_stack"] = [t["slug"] for t in r.json()["detected"]] if r.status_code == 200 else []
return record
print(enrich("jane@northwind.example"))const API = "https://api.jobspipe.dev";
const headers = { Authorization: "Bearer jp_live_your_key_here" };
async function enrich(email) {
const record = { email };
let r = await fetch(`${API}/v1/companies/${encodeURIComponent(email)}`, { headers });
if (r.status !== 200) return record;
const company = await r.json();
Object.assign(record, {
company: company.name,
domain: company.domain,
employees: company.employee_count,
country: company.location?.country ?? null,
});
const domain = company.domain ?? email.split("@")[1];
r = await fetch(`${API}/v1/companies/${domain}/technologies?tier_min=likely`, { headers });
record.hires_for = r.status === 200 ? (await r.json()).data.map((t) => t.technology.slug) : [];
r = await fetch(`${API}/v1/stack/scan`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ domain }),
});
record.website_stack = r.status === 200 ? (await r.json()).detected.map((t) => t.slug) : [];
return record;
}
console.log(await enrich("jane@northwind.example"));Notes
- A company appears only once it has posted a job we collected; one that never has is absent, not empty.
- A domain is returned only when two independent sources agree on it, so you never get another employer's details attached to your lead.
- To find new leads by technology instead, see Find companies that use a technology.