DEV Community

clause-netizen
clause-netizen

Posted on

Enrich CRM Leads From a Domain Alone: Tech Stack, Socials, and Public Emails

A lead lands in your CRM with a company name and a website. Nothing else. Someone on the team now has to open the site, squint at it, guess whether it's worth a call, and hunt for a contact address — and they'll do that a few hundred times this month. The parts that are actually machine-readable (what the site runs on, which social accounts it links, which emails it publishes) are sitting in the HTML the whole time.

SiteIntel fetches a page and hands that back as JSON. One GET, one URL in, structured fields out.

The request

Auth is standard RapidAPI headers. The url param is a full https:// URL — a bare domain won't work, so normalize before you call.

curl -s 'https://siteintel.p.rapidapi.com/v1/analyze?url=https%3A%2F%2Fstripe.com' \
  -H 'X-RapidAPI-Key: YOUR_KEY' \
  -H 'X-RapidAPI-Host: siteintel.p.rapidapi.com'
Enter fullscreen mode Exit fullscreen mode

In Node, using global fetch (18+):

const HEADERS = {
  'X-RapidAPI-Key': process.env.RAPIDAPI_KEY,
  'X-RapidAPI-Host': 'siteintel.p.rapidapi.com',
};

async function analyze(domain) {
  const url = domain.startsWith('http') ? domain : `https://${domain}`;
  const endpoint =
    `https://siteintel.p.rapidapi.com/v1/analyze?url=${encodeURIComponent(url)}`;

  const res = await fetch(endpoint, { headers: HEADERS });
  if (!res.ok) throw new Error(`SiteIntel ${res.status}: ${await res.text()}`);
  return res.json();
}

const site = await analyze('stripe.com');

console.log({
  name: site.open_graph?.site_name || site.title,
  tech: site.detected_tech,
  socials: site.social_links,
  emails: site.emails,
});
Enter fullscreen mode Exit fullscreen mode

encodeURIComponent matters here. An un-encoded https:// in a query string will get chewed up by proxies and you'll spend twenty minutes debugging a 400.

What comes back

{
  "query": "https://stripe.com",
  "final_url": "https://stripe.com/",
  "status": 200,
  "fetched_at": "2026-08-21T14:02:11Z",
  "title": "Stripe | Financial Infrastructure to Grow Your Revenue",
  "description": "Stripe powers online and in-person payment processing...",
  "canonical": "https://stripe.com/",
  "lang": "en",
  "favicon": "https://stripe.com/favicon.ico",
  "open_graph": {
    "title": "Stripe | Financial Infrastructure",
    "image": "https://images.stripeassets.com/og.png",
    "site_name": "Stripe",
    "type": "website"
  },
  "detected_tech": ["Cloudflare", "React"],
  "social_links": ["https://twitter.com/stripe", "https://www.linkedin.com/company/stripe"],
  "emails": ["press@stripe.com"],
  "server": "nginx"
}
Enter fullscreen mode Exit fullscreen mode

Three things about this shape that will save you a rewrite:

detected_tech and social_links are flat arrays of strings. Not objects with name/confidence, not a keyed map. detected_tech.includes('Shopify') is the whole check. Same for social_links — plain URL strings, so you route them yourself:

const platformOf = (u) => {
  const host = new URL(u).hostname.replace(/^www\./, '');
  return host.split('.')[0]; // twitter, linkedin, facebook, github...
};

const byPlatform = Object.fromEntries(
  site.social_links.map((u) => [platformOf(u), u])
);
Enter fullscreen mode Exit fullscreen mode

final_url is not query. It's where the fetch actually ended up after redirects. Compare the two and you get domain migrations and www/apex canonicalization for free — useful when your CRM has three records for the same company under two spellings. There's no top-level domain field; parse it off final_url if you need one.

status is the upstream site's HTTP status, not the API's. A 200 from SiteIntel with "status": 404 in the body means the request worked and the lead's page is broken. Treat those separately — an empty detected_tech on a 404 tells you nothing, but an empty one on a 200 is real signal.

emails only contains addresses the site publishes in its own HTML — hello@, sales@, press@. It is not a contact database, and it's frequently an empty array. Treat a hit as a bonus routing hint, not as your outbound list, and check the sending rules for whatever jurisdiction you're in before you mail anything you found this way.

One thing worth building with it

The obvious use is a CRM enrichment worker: pull leads with a website and no enrichment timestamp, call /v1/analyze, write the fields back.

for (const lead of await db.leadsNeedingEnrichment(200)) {
  try {
    const site = await analyze(lead.website);
    await db.updateLead(lead.id, {
      company_name: site.open_graph?.site_name ?? site.title ?? lead.company_name,
      canonical_url: site.final_url,
      tech_stack: site.detected_tech,          // store as-is, it's already strings
      linkedin: site.social_links.find((u) => u.includes('linkedin.com')) ?? null,
      public_email: site.emails[0] ?? null,
      enriched_at: site.fetched_at,
    });
  } catch (err) {
    await db.markEnrichmentFailed(lead.id, String(err));
  }
  await new Promise((r) => setTimeout(r, 250)); // be polite, stay under your plan's rate
}
Enter fullscreen mode Exit fullscreen mode

The payoff isn't the fields themselves, it's the routing you can do once detected_tech is a queryable column. If you sell a Shopify app, detected_tech @> '["Shopify"]' is your entire lead-scoring rule. If you sell an alternative to something, matching on the incumbent's name gives you a segment that's worth a different email than everyone else gets. Cloudflare and nginx tell you close to nothing about buying intent, so don't build a score that weights them.

Two adjacent endpoints, same auth and same url param, if you want more than the JSON:

# on-page SEO report — useful if your pitch is "your site has problems"
curl -s 'https://siteintel.p.rapidapi.com/v1/seo-audit?url=https%3A%2F%2Fexample.com' \
  -H 'X-RapidAPI-Key: YOUR_KEY' -H 'X-RapidAPI-Host: siteintel.p.rapidapi.com'

# screenshot — drop it into the CRM record so reps see the site without opening it
curl -s 'https://siteintel.p.rapidapi.com/v1/screenshot?url=https%3A%2F%2Fexample.com' \
  -H 'X-RapidAPI-Key: YOUR_KEY' -H 'X-RapidAPI-Host: siteintel.p.rapidapi.com'
Enter fullscreen mode Exit fullscreen mode

Cache aggressively. A company's stack doesn't change between Tuesday and Wednesday, so key on final_url and re-enrich monthly rather than on every CRM read.


Runnable examples are in the repo: github.com/clause-netizen/siteintel-api — it's also on RapidAPI if you'd rather not manage a key.

Top comments (1)

Collapse
 
morphoices profile image
MORPHOICΞS.

This is a really practical approach to CRM enrichment—especially the idea of letting the domain do most of the heavy lifting instead of asking sales teams to manually fill in all that context. ~