Guide

How to store and refresh Triguna company records

Resolve a company URL into firmographics, normalize headcount and country codes, decide which fields to store versus re-request, and avoid the platform_url mix-up.

7 min read Updated 31 July 2026

Company records are narrower than person records and change more slowly, which makes them the easier half to store, provided you get three normalizations right up front.

The three normalizations

Headcount. staff_count and staff_range are independently nullable, so neither alone is a reliable number:

export function headcount(staff: StaffInfo | null | undefined): number | null {
  if (staff?.staff_count != null) return staff.staff_count;
  const r = staff?.staff_range;
  if (!r?.start) return null;
  return r.end == null ? r.start : Math.round((r.start + r.end) / 2);
}

Store the derived number and a flag for whether it was exact. A segmentation rule that treats an estimated 350 the same as a counted 350 will eventually be questioned, and you want to be able to answer.

Country codes. locations.headquarter.country is a lowercase two-letter code ("pt"). Uppercase it before joining against ISO data, or the join silently returns nothing.

Website URLs. company_url may carry http://, https://, or no scheme at all. Normalize before storing, and store the host separately if you match on domain:

export function normalizeSite(url: string | null): { href: string; host: string } | null {
  if (!url) return null;
  const withScheme = /^https?:\/\//i.test(url) ? url : `https://${url}`;
  try {
    const u = new URL(withScheme);
    return { href: u.href, host: u.hostname.replace(/^www\./, '') };
  } catch {
    return null;
  }
}

A table that ages well

create table companies (
  company_id       text primary key,      -- stable id, not the slug
  public_id        text not null,
  name             text,
  website_host     text,                  -- normalized, www stripped
  country_code     char(2),               -- uppercased
  city             text,
  primary_industry text,                  -- industries[0].name
  headcount        integer,
  headcount_exact  boolean not null default false,
  funding_type     text,                  -- last round only
  auto_generated   boolean not null default false,
  source           text not null,
  fetched_at       timestamptz not null,
  raw              jsonb not null
);
create index on companies (website_host);

industries[0] is the primary industry. The array is ordered, so taking the first entry is correct rather than arbitrary.

Treat auto_generated as a quality gate

auto_generated: true means the page was created by the platform rather than by the company. Those records are thin and frequently wrong. Whatever the record feeds, whether scoring, routing, or display, decide deliberately what happens when this flag is set. Most teams exclude them from scoring and show them with a caveat.

if (record.auto_generated) {
  // Do not score. Surface it, flag it, and move on.
  return { usable: false, reason: 'auto_generated_page' };
}

Funding is one round, not a history

funding_data.last_funding_round is the last round only. There is no per-round history in the response. If you need a funding trajectory, you need another source; if you need “is this company venture-backed and roughly how late-stage”, the last round answers it.

Two details that break naive parsers:

  • money_raised.amount is a raw number in whole currency units and can exceed 32-bit range. Store it as bigint.
  • money_raised.currency is lowercase ("usd"). Uppercase before formatting.

Refresh policy

Company fields move slowly, which means a fixed nightly refresh is almost pure waste:

FieldRealistic cadence
company_name, company_urlOn demand only
industries, specialitiesYearly
headcountQuarterly
funding_dataQuarterly, or on a news trigger
followers_countNever store as a fact

Better than a schedule: refresh on access, with a staleness threshold. If a record is read and its fetched_at is older than your threshold, queue a refresh. Records nobody reads never cost you anything.

const STALE_AFTER_DAYS = 90;

export function needsRefresh(row: CompanyRow): boolean {
  const age = (Date.now() - row.fetched_at.getTime()) / 86_400_000;
  return age > STALE_AFTER_DAYS;
}

Next

Start with one request

Free credits on signup, no card required. Usage-based pricing after that.