VendorStacks
On this section
← All guides

Enrich a list of domains

Loop a CSV of domains through /v1/check with rate-limit handling, and write the disclosed stack back out as CSV.

Markdown mirror: /docs/guides/enrich-a-domain-list.md

The most common first job: you have a CSV of accounts, you want each row enriched with the vendors that entity discloses. Every lookup is 1 credit flat, so a 1,000-row list is exactly 1,000 credits (~$9 on the starter pack). Indexed domains answer in under a second; unindexed ones live-scan automatically at the same price — budget extra wall-clock time for those, not extra credits.

The loop

enrich.mjs (Node 18+, no dependencies)
import { readFileSync, writeFileSync } from "node:fs";

const KEY = process.env.VENDORSTACKS_KEY;
const domains = readFileSync("accounts.csv", "utf8")
  .split("\n").map(l => l.split(",")[0].trim()).filter(Boolean);

const out = [["domain", "found", "sms_vendor", "email_vendor", "payments_vendor", "evidence_url"]];

for (const domain of domains) {
  const res = await fetch(
    `https://vendorradar-production.up.railway.app
/v1/check?url=${encodeURIComponent(domain)}`,
    { headers: { Authorization: `Bearer ${KEY}` } },
  );

  if (res.status === 429) {
    // Honor Retry-After, then retry the same domain.
    const wait = Number(res.headers.get("retry-after") ?? 2);
    await new Promise(r => setTimeout(r, wait * 1000));
    domains.push(domain);
    continue;
  }
  if (res.status === 402) throw new Error("Out of credits — top up and re-run.");

  const data = await res.json();
  const first = (cat) => data.vendor_stack?.[cat]?.[0];
  out.push([
    domain,
    String(data.found),
    first("sms_messaging")?.vendor ?? "",
    first("email")?.vendor ?? "",
    first("payments")?.vendor ?? "",
    first("sms_messaging")?.evidence?.source ?? "",
  ]);
}

writeFileSync("enriched.csv", out.map(r => r.join(",")).join("\n"));

Details that matter

  • The 429 branch re-queues the same domain after Retry-After seconds — you lose no rows and no credits (rate-limited calls are never charged).
  • found: false means no disclosure was located, not "no vendors". Keep those rows and treat them as unknown.
  • The script is idempotent from the API's perspective — re-running charges again for each lookup, so checkpoint out to disk periodically for very large lists.
  • Want today's truth for a handful of hot accounts? Re-run just those with &fresh=true — same 1-credit price, just slower (15–90s; set a 120s timeout).

Full parameter reference: GET /v1/check. Rate-limit specifics: Usage limits.