DOCS / COMPANIES API
VIEW RAW

Companies API

The companies API turns a domain into an outside-in wage breakdown, builds a reusable company library, and exports it for outreach. You enrich a domain once, then list, fetch, and export it as many times as you like for free. A research lookup is the unit of cost: it is consumed only when a domain is newly enriched into your library or explicitly refreshed.

Auth

All requests use a Bearer token from your API keys page. The base URL is https://humanhours.dev/api.

Authorization: Bearer hh_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

The lookup model

A research lookup runs the enrichment: it researches the domain, estimates the role mix and headcount, pulls labour wages from official statistics, and assembles a business case. That work is what costs a lookup.

Action Lookup consumed?
POST /v1/companies (domain new to your library) Yes
POST /v1/companies (domain already owned, no ?refresh=true) No
POST /v1/companies?refresh=true Yes
POST /v1/companies/{domain}/refresh Yes
POST /v1/companies/bulk (per domain, on success only) Yes
GET /v1/companies (list / export) No
GET /v1/companies/{domain} (single record) No
GET /v1/jobs/{id} (bulk job status) No

Lookup economics

Lookups are a hard cap with no overage billing. When you reach the limit, enrichment is blocked until you upgrade. Listing, fetching, and exporting companies already in your library always keep working.

Plan Research lookups
Free / Hobby 10 lookups, one-time
Pro 100 lookups per month
Agency 500 lookups per month, pooled across workspaces
Enterprise Custom

When you hit the cap, enrichment requests return code lookup_quota_exceeded. The hint tells you to upgrade: there are no overages, so raising the limit means moving to a higher plan at /billing.

The company record

Enrichment returns an EnrichedCompany. It carries the company profile, an estimated role mix, wage data per role and country, the business case, and a per-dimension confidence breakdown.

type ConfidenceTier =
  | "fetched_cited" // pulled from a cited source this run (highest trust)
  | "official_statistic" // official labour statistic
  | "seeded_reference" // curated reference table
  | "llm_inferred" // model estimate
  | "hard_fallback"; // last-resort default (lowest trust)
 
type EnrichedCompany = {
  domain: string;
  company_name?: string;
  country?: string; // ISO-2
  industry?: string;
  headcount_estimate?: number;
  roles: {
    role: string; // canonical job function key (see below)
    headcount: number;
    confidence: ConfidenceTier;
  }[];
  wages: {
    country: string;
    role: string;
    wage_data: {
      blended_hourly_eur: number;
      gross_hourly_eur: number;
      employer_factor: number;
      hours_per_year: number;
    };
    source?: string;
    source_url?: string;
    confidence: ConfidenceTier;
  }[];
  business_case: {
    annual_labour_cost_eur?: number;
    summary?: string; // outreach-ready wage narrative
  };
  confidence: {
    overall: number; // 0..1
    wages?: number; // 0..1
    headcount?: number; // 0..1
    roles?: number; // 0..1
  };
  sources: string[]; // source URLs
};

Fields

Field Type Notes
domain string Root domain, e.g. "stripe.com". Always present.
company_name string Enriched company name.
country string ISO 3166-1 alpha-2, e.g. "US".
industry string Industry label.
headcount_estimate number Estimated total headcount.
roles array Estimated role mix. Each entry is one canonical function with a headcount.
wages array Wage data per role and country, each entry cited where a source is available.
business_case object Annual wage bill and the outreach narrative (see below).
confidence object Per-dimension confidence, all 0..1. Honest and varies with data quality.
sources string[] Source URLs used during enrichment.

Role functions

roles[].role and wages[].role use a canonical job function key:

customer_service, sales, marketing, operations, finance_admin,
human_resources, engineering_it, product, data_analytics,
logistics, legal, support_helpdesk, management, other

Business case

The business_case object is what you put in front of a prospect.

Field Type Notes
annual_labour_cost_eur number Estimated annual wage bill across the role mix.
summary string The outreach-ready wage narrative. Drop it into a first-touch message.

The per-function wage breakdown lives in roles and wages; the largest function and its hourly wage are the natural outreach hook.

Wages and confidence

Wages are sourced from official labour statistics (for example Eurostat, ONS, BLS, INSEE) where available, each cited with a source_url. Confidence is reported per dimension and is honest: a record with cited wages and a weak headcount estimate will show high confidence.wages and lower confidence.headcount. Treat every number as an outside-in estimate to validate against a company's own data, not as ground truth.

CSV columns

When you request format=csv, each row flattens one company. Columns, in order:

domain,external_id,company_name,country,industry,headcount_estimate,annual_labour_cost_eur,confidence,summary,tags,added_at,last_refreshed_at

tags is a pipe-separated list within the cell (e.g. outreach-q3|enterprise). Empty optional fields are empty strings. The first row is always the header.


POST /v1/companies

Enrich a single domain and add it to your library. This call is synchronous: it researches and enriches the domain inline (typically 15-30 seconds) and returns the full record in the response. Use the bulk endpoint for large lists.

It charges one lookup when the company is new to your library, or when you pass ?refresh=true. Returning a company you already own without refresh is free.

Request

curl -X POST https://humanhours.dev/api/v1/companies \
  -H "Authorization: Bearer $HUMANHOURS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "stripe.com",
    "external_id": "acct_123",
    "tags": ["outreach-q3", "enterprise"]
  }'

Body fields:

Field Type Required Notes
domain string Yes Root domain, e.g. "stripe.com". Subdomains are normalised automatically.
external_id string No Your stable cross-system id for this company.
tags string[] No Free-form labels for segmentation and export filtering.

Query parameters:

Parameter Default Notes
refresh false Set ?refresh=true to re-enrich and charge a lookup even if the domain is already in your library.

Response (201 when newly added)

{
  "company": {
    "domain": "stripe.com",
    "company_name": "Stripe",
    "country": "US",
    "industry": "Financial Technology",
    "headcount_estimate": 8000,
    "roles": [
      { "role": "engineering_it", "headcount": 3200, "confidence": "llm_inferred" },
      { "role": "customer_service", "headcount": 900, "confidence": "seeded_reference" }
    ],
    "wages": [
      {
        "country": "US",
        "role": "customer_service",
        "wage_data": {
          "blended_hourly_eur": 28.5,
          "gross_hourly_eur": 21.0,
          "employer_factor": 1.36,
          "hours_per_year": 1800
        },
        "source": "BLS Occupational Employment Statistics",
        "source_url": "https://www.bls.gov/oes/",
        "confidence": "official_statistic"
      }
    ],
    "business_case": {
      "annual_labour_cost_eur": 410000000,
      "summary": "Stripe runs an estimated 900-person customer service function, its largest, at around 30 EUR an hour. Total annual labour cost is in the order of 410 million. These are outside-in estimates to validate against the company's own figures."
    },
    "confidence": {
      "overall": 0.71,
      "wages": 0.9,
      "headcount": 0.6,
      "roles": 0.65
    },
    "sources": ["https://www.bls.gov/oes/", "https://stripe.com/jobs"]
  },
  "charged": true
}

201 with charged: true means the company was newly added and one lookup was consumed. When the company was already in your library and you did not pass refresh, the response is 200 with charged: false and no lookup is used. With ?refresh=true the response is 200 with charged: true.

Errors

Code When
validation_error domain missing or not a valid domain string
lookup_quota_exceeded Monthly research-lookup cap reached; upgrade to raise the limit
internal Enrichment failed unexpectedly; retry

GET /v1/companies

List your company library, or export it. Free; no lookup consumed. This is the outreach pull.

Request

# JSON
curl "https://humanhours.dev/api/v1/companies?tag=outreach-q3&limit=500" \
  -H "Authorization: Bearer $HUMANHOURS_API_KEY"
 
# CSV download
curl "https://humanhours.dev/api/v1/companies?format=csv&tag=outreach-q3" \
  -H "Authorization: Bearer $HUMANHOURS_API_KEY" \
  -o outreach-q3.csv

Query parameters:

Parameter Default Notes
format json json returns a JSON body; csv returns a CSV file download.
tag (none) Filter to companies that carry this tag.
limit 500 Number of records to return. Max 1000.

Response (200), JSON

format=json returns the library rows. Each row wraps the EnrichedCompany together with your library metadata.

{
  "companies": [
    {
      "domain": "stripe.com",
      "external_id": "acct_123",
      "tags": ["outreach-q3", "enterprise"],
      "added_at": "2026-05-31T14:00:00.000Z",
      "last_refreshed_at": "2026-05-31T14:00:00.000Z",
      "company": { "domain": "stripe.com", "company_name": "Stripe" }
    }
  ]
}

Response (200), CSV

format=csv returns a CSV file download. See CSV columns for the column order.

domain,external_id,company_name,country,industry,headcount_estimate,annual_labour_cost_eur,confidence,summary,tags,added_at,last_refreshed_at
stripe.com,acct_123,Stripe,US,Financial Technology,8000,410000000,24000000,19000000,customer_service,900,46000000,0.52,0.71,"Stripe runs an estimated 900-person...",outreach-q3|enterprise,2026-05-31T14:00:00.000Z,2026-05-31T14:00:00.000Z

Errors

Code When
validation_error limit out of range or bad format

GET /v1/companies/export

Download your entire library as a single file. Free; no lookup consumed. Unlike GET /v1/companies (capped at 1000 rows), this endpoint pages through every company you own and streams one downloadable file. Use it for the full outreach export.

Request

# CSV (default)
curl "https://humanhours.dev/api/v1/companies/export?tag=outreach-q3" \
  -H "Authorization: Bearer $HUMANHOURS_API_KEY" \
  -o companies.csv
 
# JSON
curl "https://humanhours.dev/api/v1/companies/export?format=json" \
  -H "Authorization: Bearer $HUMANHOURS_API_KEY" \
  -o companies.json

Query parameters:

Parameter Default Notes
format csv csv or json. Both return a file download.
tag (none) Filter to companies that carry this tag.

Response (200)

A file download (companies.csv or companies.json). CSV uses the same column order as the list endpoint; JSON returns { "companies": [ ... ] } with every row.


GET /v1/companies/{domain}

Fetch a single record from your library. Free; no lookup consumed. Returns 404 when the domain is not in your library.

Request

curl https://humanhours.dev/api/v1/companies/stripe.com \
  -H "Authorization: Bearer $HUMANHOURS_API_KEY"

Response (200)

{
  "domain": "stripe.com",
  "external_id": "acct_123",
  "tags": ["outreach-q3", "enterprise"],
  "added_at": "2026-05-31T14:00:00.000Z",
  "last_refreshed_at": "2026-05-31T14:00:00.000Z",
  "company": {
    "domain": "stripe.com",
    "company_name": "Stripe",
    "business_case": {
      "summary": "Stripe runs an estimated 900-person customer service function..."
    },
    "confidence": { "overall": 0.71 }
  }
}

Errors

Code When
not_found Domain not in your library (enrich it first)

POST /v1/companies/{domain}/refresh

Re-enrich a company you already own. Always charges one lookup. Use this when the data may be stale. Returns 404 when the domain is not in your library.

Request

curl -X POST https://humanhours.dev/api/v1/companies/stripe.com/refresh \
  -H "Authorization: Bearer $HUMANHOURS_API_KEY"

Response (200)

Same EnrichedCompany shape as POST /v1/companies, with charged: true.

{
  "company": { "domain": "stripe.com", "company_name": "Stripe" },
  "charged": true
}

Errors

Code When
not_found Domain not in your library
lookup_quota_exceeded Monthly research-lookup cap reached; upgrade to raise the limit

POST /v1/companies/bulk

Enrich a list of domains. This call is asynchronous: it dedupes and validates the input, queues a background job, and returns 202 immediately. Nothing is charged at submission time. The background worker charges one lookup per domain as it processes, and only on a successful enrichment. Poll GET /v1/jobs/{id} for progress.

Request

curl -X POST https://humanhours.dev/api/v1/companies/bulk \
  -H "Authorization: Bearer $HUMANHOURS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "domains": ["stripe.com", "vercel.com", "supabase.com"],
    "tags": ["outreach-q3"]
  }'

Body fields:

Field Type Required Notes
domains string[] Yes Up to 1000 domains. Duplicates and invalids are dropped.
tags string[] No Applied to every company the job enriches.

Response (202)

{
  "job_id": "job_9f2c...",
  "accepted": 3,
  "rejected": 0,
  "duplicates_removed": 0,
  "status": "queued"
}

accepted is the number of domains queued for enrichment after dedupe and validation. rejected counts invalid domains dropped; duplicates_removed counts duplicates removed from the input.

Errors

Code When
validation_error domains missing, empty, or over 1000 entries

GET /v1/jobs/{id}

Poll a bulk job for progress. Free; no lookup consumed. Poll until status is done.

Request

curl https://humanhours.dev/api/v1/jobs/job_9f2c... \
  -H "Authorization: Bearer $HUMANHOURS_API_KEY"

Response (200)

{
  "job_id": "job_9f2c...",
  "status": "running",
  "input_count": 3,
  "accepted": 3,
  "processed_count": 1,
  "error_count": 0,
  "percent": 33
}
Field Type Notes
status string queued, running, or done.
input_count number Domains in the original submission.
accepted number Domains queued after dedupe and validation.
processed_count number Domains enriched so far.
error_count number Domains that failed enrichment (no lookup charged on these).
percent number Progress, 0-100.

When status is done, pull the results with GET /v1/companies, filtered by the tag you applied.

Errors

Code When
not_found Job id does not exist

Error reference

Every error is JSON in this shape:

{
  "error": {
    "code": "lookup_quota_exceeded",
    "message": "...",
    "hint": "Upgrade your plan to raise the limit."
  }
}
Code Cause Fix
validation_error Bad or missing domain, or invalid request body Send a valid root domain and body
lookup_quota_exceeded Monthly research-lookup cap reached Upgrade at /billing; there are no overages
not_found Company or job not found Enrich the domain first, or check the job id
internal Unexpected server error Retry; if it persists, contact support

See Errors for the full catalogue.


Found a typo or want to suggest an edit? Email support@triadagency.ai.