DEV Community

clause-netizen
clause-netizen

Posted on

Enrich a CRM lead from nothing but a domain

A lead comes in with a company website and not much else. You need their tech stack to know if they're worth a demo, their Twitter and LinkedIn for the SDR to reference, and any public contact email so the record isn't a dead end. Doing that by hand takes a few minutes per lead, and it does not scale past about thirty of them.

SiteIntel fetches the page once and returns the parsed result: detected technologies, social profile links, emails published on the page, plus the usual metadata. One request per domain.

The request

Auth is RapidAPI headers. The url param takes a full https:// URL, not a bare domain, which trips people up on the first call.

curl -s "https://siteintel.p.rapidapi.com/v1/analyze?url=https://stripe.com" \
  -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \
  -H "X-RapidAPI-Host: siteintel.p.rapidapi.com"
Enter fullscreen mode Exit fullscreen mode

Node, using global fetch (18+, no dependencies):

const HOST = "siteintel.p.rapidapi.com";

async function enrich(domain) {
  const url = `https://${HOST}/v1/analyze?url=${encodeURIComponent(`https://${domain}`)}`;
  const res = await fetch(url, {
    headers: {
      "X-RapidAPI-Key": process.env.RAPIDAPI_KEY,
      "X-RapidAPI-Host": HOST,
    },
  });

  if (!res.ok) throw new Error(`${domain}: HTTP ${res.status}`);
  const data = await res.json();

  return {
    company: data.open_graph?.site_name || data.title,
    description: data.description,
    stack: data.detected_tech ?? [],
    socials: data.social_links ?? [],
    emails: data.emails ?? [],
    finalUrl: data.final_url,
  };
}

console.log(await enrich("stripe.com"));
Enter fullscreen mode Exit fullscreen mode

Wrap the loop in a small concurrency limit if you're running a list. Sequential is fine for a few hundred rows overnight; four at a time is plenty for a nightly sync.

Reading the response

{
  "query": "https://stripe.com",
  "final_url": "https://stripe.com/",
  "status": 200,
  "fetched_at": "2026-07-24T14: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.stripecdn.com/...",
    "site_name": "Stripe",
    "type": "website"
  },
  "detected_tech": ["Cloudflare", "React"],
  "social_links": ["https://twitter.com/stripe", "https://www.linkedin.com/company/stripe"],
  "emails": [],
  "server": "nginx"
}
Enter fullscreen mode Exit fullscreen mode

Two things worth knowing before you write your mapping code.

detected_tech and social_links are flat arrays of strings. No nested objects, no confidence scores, no category field. Sorting a social link into a CRM column means matching on the hostname yourself:

const bucket = (links) =>
  Object.fromEntries(
    links.map((u) => {
      const h = new URL(u).hostname.replace(/^www\./, "");
      if (h.includes("linkedin")) return ["linkedin", u];
      if (h.includes("twitter") || h.includes("x.com")) return ["twitter", u];
      if (h.includes("facebook")) return ["facebook", u];
      if (h.includes("instagram")) return ["instagram", u];
      if (h.includes("youtube")) return ["youtube", u];
      return ["other", u];
    })
  );
Enter fullscreen mode Exit fullscreen mode

emails is often empty, and that's the honest answer rather than a guess. It reflects addresses published on the page that was fetched. Plenty of companies route everything through a contact form, so treat a hit as a bonus and never as a required field in your schema.

Also compare final_url against what you sent. A redirect to a different apex domain usually means the company got acquired or rebranded, which is a useful flag on its own.

What to build with it

The thing I actually run: a nightly job that walks CRM records where enriched_at is null, calls /v1/analyze on the website field, and writes back three columns. Stack goes into a tag list, socials into their own fields, and the description becomes the first line of context an SDR sees.

Routing off detected_tech is where it earns its keep. If you sell a Shopify app, a lead whose array contains "Shopify" is a different lead from one running WordPress, and that decision is a single .includes() at ingest time instead of a rep opening a tab. Same trick for scoring: leads on Cloudflare and React skew toward having an actual engineering team, which matters if your product needs one to adopt it.

Cache aggressively. A company's stack does not change between Tuesday and Wednesday, so store the payload and re-fetch monthly. Store the raw JSON alongside your mapped columns too, because the first time you want a field you didn't map, you'll want the original response instead of a re-crawl.

Two other endpoints on the same key are useful in a lead context. GET /v1/seo-audit?url=https://example.com gives an on-page report, which is a decent talking point if you sell anything marketing-adjacent. GET /v1/screenshot?url=https://example.com gets you a thumbnail for the CRM record.

Working examples, including the batch runner: https://github.com/clause-netizen/siteintel-api — it's on RapidAPI too if you'd rather not host anything: https://rapidapi.com/hidanny0001/api/siteintel

Top comments (0)