Skip to content

REST Reference (legacy)

Legacy interface — prefer DSL

The REST API is the original, legacy surface. It remains supported for existing integrations, but it is a subset of the DSL and does not receive new capabilities first. New integrations should use DSL.

Beta — subject to change

Breaking changes are announced in the Changelog.

The full REST reference is included below (rendered from the maintained external source document).


SFF Confidential

You agree to keep all Data strictly confidential and protect it with the same level of care you use to protect your own confidential information, but no less than reasonable care. Access to the Data is limited to your employees, contractors or agents who are informed of and agree to these confidentiality obligations. You are not permitted to disclose, distribute, publish, or otherwise share the Data, in any manner or medium, with any third party, without the prior written consent of the Foundation. You may not use the Data for commercial purposes.


AOI REST API Reference

Version: 0.23.0-beta
Base URL: https://aonav.ai
Protocol: HTTPS
Authentication: JWT Bearer token
Last Updated: 2026-10-26

Changes since 0.15.16-beta (previous external doc revision). Full detail, with before/after examples and a client checklist, is in REST_API_DELTA_0.15.16_TO_0.23.0.md. - Breaking — 0.17.0-beta (occupation data refresh): wage rows move from three tenure points (0, 5, 10) to eleven (years_in 0–10) per MSA size. Clusters and ONET codes withdrawn by the provider now return not-found. - Breaking for MCP clients — 0.16.0-beta / 0.16.10-beta: POST /mcp/v1 is removed (use POST /mcp); MCP tool results need one json.loads, not two; the format tool argument is retired (still accepted and ignored). - Breaking — 0.16.7-beta: GET /api/occupations/cluster:{id}/pathways returns 410 Gone. Use /api/clusters/{id}/cluster-destinations. - 0.23.0-beta (additive): AI impact scores (ai_impact_scores) on cluster detail and both AI-impact endpoints; cluster_description and premium_skills_summary; premium_skill_description_1/2/3 are now always null. The cluster set is refreshed (794 clusters). - 0.17.5-beta (additive): wage_growth block on the wage endpoints and cluster detail. - 0.17.7-beta (additive): display_name / display_source on every /api/titleconvert row. - 0.16.8 / 0.16.11 / 0.17.0-beta (adjacency): data_status on adjacency rows; destinations widen from 3 to 5; the neighbour set is curated, not a sample. - 0.16.1 / 0.16.2-beta (TITLECONVERT): results are distinct clusters; match_source may be compound (name_corpus+catalog_exact); boolean query params accept 1/true/yes/on. - 0.17.0-beta:* ai_flag is deprecated (still served and filterable).

Changes 0.11.7-beta → 0.15.16-beta (kept for reference): - 0.12.0-beta + 0.12.1-beta: New cross-company badge analytics endpoints /api/companies/badges and /api/occupations/badges (+ /counts variants that return per-archetype × tier counts and the 3×3 matrix in one call). See Badge Analytics Endpoints below. - 0.12.2-beta (breaking, scoped): The derived education_level response field has been removed from /api/occupations/{id}/education and /api/clusters/{id}/education. Clients should read ba_plus_share (raw 0.0–1.0 share of workforce with a BA) and/or job_level (Sub-Bachelor's Degree, Bachelor's Degree, Advanced Degree, optional + Experience). - 0.12.3-beta (fix): The Ranked badge sentinel now works consistently on every badge-filterable surface — including /api/clusters/{id}/companies, /api/occupations/{id}/companies, and /api/companies/{name}/occupations. Previously these three returned 0 rows for ?badge_X=Ranked (the value was treated as a literal string and matched nothing). Same semantic as everywhere else: matches any non-null tier (Platinum + Gold). - 0.12.4-beta: /api/clusters/{id}/companies and /api/companies/{name}/clusters now return each company's primary_industry; the former gains a sort=industry mode. - 0.12.5-beta: New calculated sort=wywm mode on /api/clusters/{id}/companies — ranks companies by a fixed badge×hiring priority, with an optional archetype selector (early_career default / growth / stability). See the endpoint below. Bucket order may be tuned during the beta. - 0.13.0-beta (schema / breaking — 6-2-26 BGI update): On company_occupation_summary the occupation-level postings_count_qtile (1–5 quintile) is retired and replaced by five fields — entry_posting_count, entry_posting_tier, total_posting_count, total_posting_tier (tiers: high / above-average / average / low / none), and early_career_access (0/1, response-only). The old ?postings_qtile= filter and ?sort=postings_qtile now return HTTP 400 with a structured deprecation error — use ?entry_posting_tier= / ?total_posting_tier= and ?sort=entry_postings / ?sort=total_postings. The sort=wywm hiring signal is now the textual tier and is archetype-dependent (early_career → entry_posting_tier; growth/stability → total_posting_tier). New opt-in ?expand=timestamps adds created_at / updated_at to /api/companies and /api/companies/{name}/occupations. The geographic hiring_postings_qtile (1–3 on hiring_flag) is a different column and is unchanged. - 0.13.1-beta (feature — additive): /api/titleconvert gains an opt-in ?expand=cluster that attaches the occupation cluster (cluster_id + canonical cluster_name) to each match, resolving title → ONET → cluster in one call. The cluster name matches what /api/clusters/convert returns. Omitted unless requested. SUBJECT TO CHANGE. - 0.14.8-beta (behavioral — no-company /api/titleconvert): results are now de-duplicated by ONET on the no-company path, so LIMIT counts distinct occupations (the flat per-row schema is preserved; company_name is the top company). Company-scoped calls and ?group=onet are unchanged. - 0.15.0-beta (behavioral — default ON; reversible): fast typeahead mode for the no-company /api/titleconvert path — a word-prefix match over the survey NAME corpus (~100–1000× faster), returning distinct occupations (match_source=name_corpus). Multi-word phrases that don't resolve cleanly defer to the legacy pipeline. Toggle per request with ?_fast=true|false. No response-shape change. SUBJECT TO CHANGE. - 0.15.0-beta (experimental toggles): /api/titleconvert accepts opt-in matcher toggles ?_catalog, ?_fusion, ?_bypass, ?_fts_and, ?_stem (and ?_fusion_alpha|_fusion_beta|_fusion_exact|_fusion_lex_mode). Absent => server default. These are experimental A/B levers and WILL CHANGE. - 0.15.1-beta (note — DSL-only feature): a batch title-conversion form exists on DSL/MCP only (TITLECONVERT ["a","b",...], ≤200 titles/call). REST /api/titleconvert remains single-title; call it once per title, or use the DSL/MCP interface for bulk resolution. - 0.15.2-beta (behavioral — match_score scale): on the /api/titleconvert database / fusion path the match_score is now 1.0-anchored — the best match anchors at 1.0 and the remaining gated results scale relative to it. This removes the earlier ~0.50 score plateau that multi-word queries (no exact survey phrase and no single leading term) used to hit, which made intra-cluster ranking noisy and non-comparable. The response shape is unchanged (match_score / confidence); the fast typeahead (match_source=name_corpus) path was already 1.0-anchored and is unaffected. Callers that hard-coded a fixed min_score cutoff should re-check it against the new scale (see the min_score note under the endpoint). - 0.15.15-beta (feature — additive): POST /api/titleconvert records which result the user picked from a prior GET. Body piggybacks the GET envelope plus picked (the chosen data[] row). Does not re-run matching. POST only (GET is cached at the proxy). DSL twins: SUBMIT TITLECONVERT TITLE "..." CHOICE "<onet_code>" and SUBMIT TITLECONVERT VALUE '{...same JSON...}' (0.15.16). - 0.15.16-beta (DSL parity — REST unchanged): GET AI_IMPACT FOR CLUSTER "187" is now the DSL twin of GET /api/clusters/{id}/ai-impact. REST path and body are unchanged.

MCP note: the MCP surface is a single tool, aoi_dsl_query (0.14.2-beta), served at POST /mcp only — POST /mcp/v1 returns 404 since 0.16.0-beta. /mcp always requires a Bearer token and returns tool errors as HTTP 200 with the error in the JSON-RPC body. Since 0.16.10-beta content[0].text parses to the DSL envelope in one json.loads (also offered as structuredContent), and the format argument is retired. Issue TITLECONVERT "..." / COMPANYCONVERT "..." as DSL through aoi_dsl_query. (REST endpoints are unaffected.)

Access: Contact tech@schultzfamilyfoundation.org for API credentials.


Methodology

The data behind this API comes from the Where You Work Matters (WYWM) project, a joint project of the Burning Glass Institute and the Schultz Family Foundation. The methodology was developed in partnership with the Managing the Future of Work Project at Harvard Business School.

Coverage

Dimension Value
Companies ~1,750 U.S. employers
Occupations 948 unique O*NET-SOC codes
Company × occupation-cluster pairs assessed ~38,500
Occupation clusters with published attributes (wages, skills, descriptions) 794
Occupation clusters a title or O*NET code can resolve to 815
Industries 32
Observation window Predominantly 2019–2024

Clusters are not O*NET classifications. A WYWM cluster is an internal cohort grouping that clusters ONET occupations by how they actually behave in the labor data — distinct from (and faster-moving than) the rigid ONET taxonomy. The cluster set is refreshed with each provider delivery (most recently 2026-10-23): clusters can be withdrawn and later return, so treat a not-found on a previously valid cluster_id as a normal outcome. A small set of clusters (for example military occupations and Chief Executives) resolve from titles but carry no attribute data; see data_status under the adjacency endpoints.

Company badges

Each company carries up to four badges. Each badge is independent — a company can be Platinum overall but Gold for growth.

Field What it captures
overall_badge Weighted combination across the three archetype scores. Early Career and Growth are down-weighted by 0.75 to account for shared underlying metrics.
badge_early_career Quality of opportunity for entry-level workers
badge_growth Wage and role-progression dynamics
badge_stability Tenure, retention, and durability of opportunity

Values: Platinum (top fifth), Gold (next fifth), or null (insufficient data — not "low quality").

The Ranked filter value matches any company with a non-null badge on the targeted field (i.e., either Platinum or Gold).

Occupation badges

Per–(company × occupation) badges share the company-level vocabulary (Platinum / Gold / null) but are computed against the (company × occupation) cohort rather than the cross-company distribution — i.e. a company is Platinum for a specific role when its outcomes for that role sit in the top fifth of all employers offering it.

Value Meaning
Platinum Top 20% of (company × occupation) outcomes for that role
Gold Next 20%
null Insufficient data, or below the threshold

Most platinum-grade opportunities live at the (company × occupation) tier — not at the company-wide rollup. Use /api/companies/{name}/occupations and /api/companies/{name}/badges to see role-level signal.

AI impact scores (0.23.0-beta)

Three continuous scores per cluster, from the provider's 2026-10-23 AI impact research (methodology_version 2026-10-23):

Score Scale Meaning
demand_score −5 to +5 AI's expected effect on overall demand for the work
entry_level_score −5 to +5 Shift toward (+) or away from (−) entry-level roles
skills_change_score 0 to 5 How much the required skills change

Coverage. 169 of 794 clusters carry scores. A cluster score is either taken directly from its single occupation (method = Direct (single occupation)) or is a posting-weighted average of several (Weighted avg by monthly postings). Where a cluster has occupations the score was not computed from, they are listed and the score should not be applied to them. 14 clusters publish no entry-level score and 1 publishes no skills-change score; those fields are null.

Presentation. Quote scores as published, to one decimal. The provider publishes no bands or labels (no "high/moderate/low"); do not invent them. For a cluster with no scores, the response carries the message to show: "AI impact information not available at this time." See the ai_impact_scores block.

AI flag (deprecated 0.17.0-beta)

Deprecated. ai_flag keeps working, keeps its filter and keeps its place in every payload, but no new deliveries update it. For new work prefer the AI impact scores above.

ai_flag classifies a cluster on two axes: barrier-to-entry trend and value-of-expertise trend. The full expertise narratives and the underlying trend fields are exposed via /api/occupations/{id}/ai-impact and /api/clusters/{cluster_id}/ai-impact.

ai_flag Barrier to entry Expertise premium Coverage
Raising the Bar Rising Rising 63 clusters
Shrinking Fields Rising Falling 25 clusters
Winners Pull Away Falling Rising 23 clusters
Lower Potential Falling Falling 65 clusters

Coverage caveat. 176 clusters carry an AI-impact classification at methodology version 2026-04-21. The remaining clusters return null for ai_flag and the trend / narrative fields, with a coverage sub-block flagged classified: false — surface as "not yet classified," not as an error.

Both AI overlays are separate Burning Glass Institute research projects that build on the WYWM core methodology. Treat them as distinct overlays with their own methodology version stamps and their own coverage profiles; a cluster can have one, both or neither.

Wage data

Wage tables are returned per cluster, with one row per tenure year (years_in 0–10, eleven rows) for each metro size (msa_size: Large, Small/Medium). A separate cluster-level wage_growth block reports median growth from year 0 to year 10; it is weighted differently from the per-row percentiles and does not reproduce from them (see the wage endpoint). The wage table is distinct from the company-level wage signals that feed the Stability badge.

Limitations

The dataset is built from self-reported career histories at scale and inherits the constraints of that source:

  • Self-report bias on titles, tenure, and dates
  • Under-representation of contract-employee work and very short stints
  • Geographic concentration in metro areas with the strongest signal
  • Skill development pursued outside the workplace is not captured

For a deeper methodology discussion, the canonical WYWM source is whereyouworkmatters.org.


API Stability

Component Status
Core REST Endpoints ✅ Stable
Search & Conversion Tools ✅ Stable
Cross-company Badge Analytics (/api/companies/badges, /api/occupations/badges + /counts) ✅ Stable (added 0.12.1-beta)
Cluster Pathway Endpoints ⚠️ Beta
Adjacency Endpoints (skill-adj-clusters, cluster-destinations) ⚠️ Beta
AI-impact narrative + trend surface ⚠️ Beta — pre-1.0; ai_flag deprecated 0.17.0-beta
AI impact scores (ai_impact_scores) ⚠️ Beta — added 0.23.0-beta; SUBJECT TO CHANGE as the provider's methodology evolves
WYWM calculated sort (/api/clusters/{id}/companies?sort=wywm) ⚠️ Beta — added 0.12.5-beta; the badge×hiring bucket order may be tuned

All endpoints maintain backward compatibility within the current major release. Additive changes (new optional parameters, new response fields) are not considered breaking; clients should ignore unknown keys. Data refreshes from the provider can change row counts and withdraw or restore clusters. Scoped breaking changes are listed at the top of this document with their migration path (most recently 0.17.0-beta, the move to eleven wage rows per MSA size).


Quick Start

1. Authenticate

curl -X POST "https://aonav.ai/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"username":"{your_username}","password":"{your_password}"}'

2. Use the Token

curl -H "Authorization: Bearer {access_token}" \
  "https://aonav.ai/api/companies?limit=10"

Endpoints

Authentication

Method Endpoint Description
POST /auth/login Get access token
POST /auth/refresh Refresh token
POST /auth/logout Invalidate token

Companies

Method Endpoint Description
GET /api/companies List companies
GET /api/companies/{name} Company details
GET /api/companies/{name}/badges Badge breakdown for one company
GET /api/companies/badges Cross-company badge rows (0.12.1-beta)
GET /api/companies/badges/counts Cross-company badge counts + 3×3 matrix (0.12.1-beta)
GET /api/companies/{name}/occupations Occupations at company
GET /api/companies/{name}/clusters Cluster analytics
GET /api/companies/{name}/hiring Hiring data
GET /api/companies/search Search companies

Industries

Method Endpoint Description
GET /api/industries List industries

Occupations

Note: {id} accepts O*NET code (15-1252.00) or cluster prefix (cluster:187)

Method Endpoint Description
GET /api/occupations List occupations
GET /api/occupations/{id} Occupation or cluster details
GET /api/occupations/{id}/wages Wage data (occupation or cluster)
GET /api/occupations/{id}/companies Companies with this role
GET /api/occupations/{id}/clusters Cluster membership
GET /api/occupations/{id}/ai-impact AI impact (occupation or cluster)
GET /api/occupations/{id}/education Education (occupation or cluster)
GET /api/occupations/{id}/pathways Career pathways (occupation or cluster)
GET /api/occupations/badges Cross-employer (company × occupation) badge rows (0.12.1-beta)
GET /api/occupations/badges/counts Per-pair badge counts + 3×3 matrix (0.12.1-beta)

Clusters

Method Endpoint Description
GET /api/clusters List clusters
GET /api/clusters/{id} Cluster details
GET /api/clusters/{id}/education Education data for cluster
GET /api/clusters/{id}/wages Wage data for cluster
GET /api/clusters/{id}/retention Retention data for cluster
GET /api/clusters/{id}/companies Companies hiring for this cluster (geo + sort filters)
GET /api/clusters/{id}/skill-adj-clusters Clusters sharing the baseline's skill profile (with optional metric filters)
GET /api/clusters/{id}/cluster-destinations Clusters people in the baseline cluster typically transition into

Location

Method Endpoint Description
GET /api/location/zip/{zip} Location by ZIP code
GET /api/location/cbsa/{cbsa} Location by CBSA code

Search Tools

Method Endpoint Description
GET /api/titleconvert Job title → O*NET
POST /api/titleconvert Record which TITLECONVERT result the user picked
GET /api/companyconvert Fuzzy company match
GET /api/clusters/convert Job title → occupation clusters

System

Method Endpoint Description
GET /health Health check (no auth)
GET /api/version Version info

Authentication

Login

POST /auth/login

{
  "username": "{your_username}",
  "password": "{your_password}"
}

Response:

{
  "success": true,
  "tokens": {
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
    "token_type": "Bearer",
    "expires_in": 86400,
    "session_id": "83fe312f-136e-4cf7-b5b9-7f3d918d73b6"
  },
  "session_id": "83fe312f-136e-4cf7-b5b9-7f3d918d73b6",
  "user": {
    "username": "{your_username}",
    "role": "{your_role}",
    "permissions": ["companies:read", "occupations:read", "wages:read", "dsl:execute"]
  }
}

Session ID: Unique identifier for this login session. Persists across token refreshes.

Token Lifetimes: - Access Token: 24 hours (86,400 seconds) - Refresh Token: 90 days

Using Tokens

Include in all requests:

Authorization: Bearer {access_token}

Refresh Token

POST /auth/refresh

{
  "refresh_token": "{refresh_token}"
}

Company Endpoints

GET /api/companies

List companies with filters and sorting. Supports multiple simultaneous filters connected by &.

Filter Parameters:

Name Type Values Description
industry string Industry name Filter by primary industry (e.g., Healthcare, Software & Technology)
industry_id integer Hash ID Filter by industry hash ID (see /api/industries for IDs)
sector string Industry name Legacy alias for industry - use industry instead
overall_badge string Platinum, Gold, Ranked Filter by overall badge
badge_early_career string Platinum, Gold, Ranked Filter by early career badge
badge_growth string Platinum, Gold, Ranked Filter by growth badge
badge_stability string Platinum, Gold, Ranked Filter by stability badge
search string Partial text Search in company name
event_type string Event type Filter by corporate event (see values below)
company_uid string UID code(s) Filter by company UID. Single: AN85. Multiple (comma-separated): AN85,OL1194,TH1653

Corporate Event Types (for event_type filter):

Value Meaning
none No corporate event (default for most companies)
acquired Company was acquired and absorbed
acquired_still_operating Acquired but still operating under its own brand
merged Merged with another company
rebranded Company rebranded to a new name
split Company split into separate entities
chapter_11_bankruptcy Filed for Chapter 11 bankruptcy

Special Badge Value: Ranked

Using Ranked as a badge value filters for companies that have any non-NA badge (either Platinum or Gold). Works for ALL badge fields including overall_badge:

# Companies with ANY overall badge (Platinum or Gold)
GET /api/companies?overall_badge=Ranked

# Companies with ANY growth badge
GET /api/companies?badge_growth=Ranked

# Healthcare companies that have any stability badge
GET /api/companies?industry=Healthcare&badge_stability=Ranked

Pagination Parameters:

Name Type Description
limit int Max results (default: 50, max: 1000)
offset int Pagination offset (default: 0)

Sorting Parameters:

order_by Value Description
alpha Alphabetical A→Z (default)
alpha_desc / name_desc Reverse alphabetical Z→A
platinum Most Platinum badges first (weighted scoring)
gold Most Gold badges first (weighted scoring)
badge_count Most badges (any level) first
industry Sort by industry, then name
Parameter Type Description
include_score boolean When true, adds badge_sort_score to each company (diagnostic for platinum/gold sorting)

Examples:

# Basic: Platinum companies in Healthcare
GET /api/companies?industry=Healthcare&overall_badge=Platinum&limit=20

# Complex: Healthcare + Platinum overall + any growth badge, sorted by platinum
GET /api/companies?industry=Healthcare&overall_badge=Platinum&badge_growth=Ranked&order_by=platinum&limit=10

# Diagnostic: Platinum sort with score visibility
GET /api/companies?order_by=platinum&include_score=true&limit=10

# Multi-badge: Insurance companies with ALL badges ranked
GET /api/companies?industry=Insurance&badge_growth=Ranked&badge_stability=Ranked&badge_early_career=Ranked&limit=10

# Banks with Gold overall and any growth badge
GET /api/companies?industry=Banks%20%26%20Financial%20Services&overall_badge=Gold&badge_growth=Ranked&limit=10

# Sort by most Platinum badges
GET /api/companies?order_by=platinum&limit=10

# Reverse alphabetical
GET /api/companies?order_by=name_desc&limit=10

# Companies that were acquired
GET /api/companies?event_type=acquired&limit=10

# Companies that merged
GET /api/companies?event_type=merged&limit=10

# Acquired companies in Technology
GET /api/companies?event_type=acquired&industry=Technology&limit=10

Response:

{
  "success": true,
  "data": [
    {
      "company_uid": "EX123",
      "company_name": "Example Corp",
      "company_description": "A leading technology company...",
      "company_URL": "https://example.com",
      "primary_industry": "Technology",
      "secondary_industry": "Software",
      "company_city": "Seattle",
      "company_state": "WA",
      "company_country": "US",
      "overall_badge": "Platinum",
      "badge_early_career": "Gold",
      "badge_growth": "Platinum",
      "badge_stability": "Platinum",
      "company_event_type": "acquired",
      "company_event_note": "Acquired by Mega Corp in 2025",
      "aoi_parents": ["MC456"],
      "aoi_subsidiaries": ["SB789", "SB790"],
      "aoi_siblings": ["SI101"]
    }
  ],
  "count": 1
}

Note: company_event_type and company_event_note are only present when non-empty. The aoi_parents, aoi_subsidiaries, and aoi_siblings fields are always returned as arrays of company_uid values (empty array [] when none).

Expanding Relationships

Add ?expand=relationships to resolve UIDs to company names:

GET /api/companies?event_type=acquired&expand=relationships&limit=5
GET /api/companies/Amazon?expand=relationships

When expanded, the response includes a related_companies block:

{
  "aoi_parents": ["MC456"],
  "aoi_subsidiaries": ["SB789", "SB790"],
  "related_companies": {
    "parents": [{"company_uid": "MC456", "company_name": "Mega Corp"}],
    "subsidiaries": [
      {"company_uid": "SB789", "company_name": "SubCo Alpha"},
      {"company_uid": "SB790", "company_name": "SubCo Beta"}
    ],
    "siblings": []
  }
}

The original aoi_* UID arrays remain unchanged; related_companies is additive.

Company Response Fields

Field Type Description
company_uid string Unique company identifier (e.g., AN85)
company_name string Company name
company_description string Company description
company_URL string Company website URL
primary_industry string Primary industry classification
secondary_industry string Secondary industry (if applicable)
company_city string Headquarters city
company_state string Headquarters state
company_country string Headquarters country
overall_badge string Overall WYWM badge (Platinum, Gold, or null)
badge_early_career string Early career badge
badge_growth string Growth badge
badge_stability string Stability badge
company_event_type string Corporate event type (omitted when none)
company_event_note string Event details (omitted when empty)
aoi_parents array Parent company UID(s) — empty array [] when none
aoi_subsidiaries array Subsidiary company UID(s) — empty array [] when none
aoi_siblings array Sibling company UID(s) — empty array [] when none
related_companies object Resolved names (only present when ?expand=relationships)

GET /api/companies/{name}

Get company details.

Example:

GET /api/companies/Amazon

GET /api/companies/{name}/badges

Get badge breakdown for a company. Data from companies table.

Response:

{
  "success": true,
  "data": {
    "company_name": "Microsoft",
    "overall_badge": "Platinum",
    "badge_early_career": "Gold",
    "badge_growth": "Platinum",
    "badge_stability": "Platinum"
  }
}

See Badge Analytics Endpoints below for the cross-company forms (/api/companies/badges, /api/companies/badges/counts) that return badge rows across many companies in a single call — and for the 3×3 archetype × tier matrix.

GET /api/companies/badges (0.12.1-beta)

Cross-company badge rows. Returns one row per company with the four badge fields (overall_badge, badge_early_career, badge_growth, badge_stability) plus identity columns, filterable by archetype tier, industry, and location. Use this form when you want to materialize "all companies that look like X" in a single call rather than iterating per company via /api/companies/{name}/badges.

Parameters:

Name Type Description
overall_badge string Platinum, Gold, or Ranked (any non-null). Comma-separated for OR (e.g. Platinum,Gold).
badge_early_career string Same vocabulary as overall_badge.
badge_growth string Same vocabulary.
badge_stability string Same vocabulary.
industry string Exact industry name. Comma-separated for OR.
company_state string Full U.S. state name (e.g. Washington, not WA).
company_city string City name.
company_country string Country name.
limit int Max results (default 100, max 1000).
offset int Pagination offset (default 0).

Examples:

# Platinum-growth companies in Healthcare
GET /api/companies/badges?industry=Healthcare&badge_growth=Platinum&limit=20

# Multi-value: Platinum OR Gold growth (OR semantics via comma-split)
GET /api/companies/badges?badge_growth=Platinum,Gold&limit=20

# Texas employers with ANY overall badge
GET /api/companies/badges?company_state=Texas&overall_badge=Ranked&limit=100

Response shape: { "success": true, "data": [ {company_name, overall_badge, badge_early_career, badge_growth, badge_stability, industry, company_state, ...}, ... ], "count": <int>, "total": <int> }

GET /api/companies/badges/counts (0.12.1-beta)

Scalar counts and per-group rollups over /api/companies/badges. Accepts the same filter params as the list endpoint, plus group_by for grouped output.

Parameters (in addition to all /api/companies/badges filter params):

Name Type Description
group_by string One of archetype, industry, overall_badge, company_state. Omit for a scalar total.

group_by=archetype returns the 3×3 archetype × tier matrix in one call:

{
  "success": true,
  "data": {
    "group_by": "ARCHETYPE",
    "by_archetype": {
      "early_career": { "platinum": 351, "gold": 351, "ranked": 702 },
      "growth":       { "platinum": 352, "gold": 350, "ranked": 702 },
      "stability":    { "platinum": 350, "gold": 351, "ranked": 701 }
    },
    "total_rows": 1752
  }
}

The ranked cell on each archetype = the count of any non-null tier on that archetype (Platinum + Gold), matching the Ranked filter sentinic semantic.

Examples:

# Scalar count of Platinum-growth companies
GET /api/companies/badges/counts?badge_growth=Platinum

# 3×3 archetype × tier matrix across all assessed companies
GET /api/companies/badges/counts?group_by=archetype

# Same matrix, but scoped to Healthcare
GET /api/companies/badges/counts?industry=Healthcare&group_by=archetype

# Per-industry tally of Platinum-growth companies
GET /api/companies/badges/counts?badge_growth=Platinum&group_by=industry

Error handling: Unknown group_by values return HTTP 400 with the structured error body under data.error, including valid_group_by, suggestion, and example so clients can self-correct.

GET /api/companies/{name}/occupations

List occupations at a company.

Parameters:

Name Type Description
group string occupation to group results
limit int Max results
onet_code string Filter by O*NET code
cluster_id int Filter by cluster ID
top10 string 1 = show only top 10 most prevalent occupations
entry_posting_tier string Filter by entry-level hiring tier: high, above-average, average, low, none
total_posting_tier string Filter by total hiring tier (same vocabulary)
badge_early_career string Filter by early career badge. Values: Platinum, Gold, or Ranked (any non-null tier).
badge_growth string Filter by growth badge. Same value vocabulary as badge_early_career.
badge_stability string Filter by stability badge. Same value vocabulary as badge_early_career.
expand string timestamps adds created_at / updated_at (ISO-8601) to each row. SUBJECT TO CHANGE.

Ranked sentinel — same semantic as /api/companies?badge_X=Ranked: matches occupations at this company that hold any non-null tier on the targeted archetype. Use Ranked when "has any growth badge for this role" matters more than the specific tier. Honored on this endpoint as of 0.12.3-beta — earlier releases returned 0 rows for this query.

DEPRECATION (6-2-26): the postings_qtile (1–5 quintile) filter was retired — ?postings_qtile= now returns HTTP 400 with a structured deprecation error. Use entry_posting_tier / total_posting_tier (values high|above-average|average|low|none). SUBJECT TO CHANGE.

Examples:

GET /api/companies/Amazon/occupations?limit=20
GET /api/companies/Amazon/occupations?top10=1
GET /api/companies/Amazon/occupations?total_posting_tier=high&limit=10
GET /api/companies/Amazon/occupations?entry_posting_tier=high&limit=10
GET /api/companies/Amazon/occupations?top10=1&badge_growth=Platinum
GET /api/companies/Amazon/occupations?expand=timestamps&limit=10
# Any-tier growth at the company (Ranked sentinel — 0.12.3-beta)
GET /api/companies/Amazon/occupations?badge_growth=Ranked

GET /api/companies/{name}/hiring

Get hiring data for a company showing job postings by location and occupation cluster.

Parameters:

Name Type Description
cluster_id int Filter by occupation cluster ID
cbsa string Filter by CBSA code (metro area)

Examples:

GET /api/companies/Amazon/hiring
GET /api/companies/Amazon/hiring?cluster_id=187
GET /api/companies/Amazon/hiring?cbsa=42660

Response:

{
  "success": true,
  "data": [
    {
      "company_name": "Amazon",
      "cluster_name": "Software Engineers",
      "cluster_id": 187,
      "cbsa_name": "Seattle-Tacoma-Bellevue, WA",
      "cbsa": 42660,
      "postings_count_qtile": "3"
    }
  ],
  "count": 54
}

GET /api/companies/{name}/clusters

Cluster-level badge analytics for a company.

Example:

GET /api/companies/Amazon/clusters

Response:

{
  "success": true,
  "data": [
    {
      "cluster_id": 187,
      "cluster_name": "Software Engineers",
      "employment_share": 0.15,
      "primary_industry": "Retail & E-Commerce",
      "badge_early_career": "Platinum",
      "badge_growth": "Gold",
      "badge_stability": "Platinum"
    }
  ],
  "count": 12
}

primary_industry (added 0.12.4-beta) is the company's industry. It is a company-wide attribute, so it is the same on every row of this single-company response. It is null for companies without an industry classification.


Industry Endpoints

GET /api/industries

List all industries with company counts and stable industry_id for filtering.

Parameters:

Name Type Description
search string Filter by name
order_by string industry or company_count
limit int Max results

Example:

GET /api/industries?order_by=company_count&limit=10

Response:

{
  "success": true,
  "data": [
    {"industry_id": 352526, "industry": "Banks & Financial Services", "company_count": 163},
    {"industry_id": 758748, "industry": "Aerospace & Defense", "company_count": 33}
  ]
}

Note: industry_id is a deterministic hash of the industry name. Use for stable filtering instead of industry names (which may contain special characters).


Occupation Endpoints

Occupation analytics endpoints accept either an O*NET code or a cluster ID with the cluster: prefix. This allows querying analytics for a single occupation or all occupations in a cluster.

GET /api/occupations

List occupations.

Parameters:

Name Type Description
search string Search title
cluster_id int Filter by cluster
job_level string Filter by education level (e.g., "Bachelor's Degree")
ai_flag string Filter by AI impact category (exact match). Values: "Raising the Bar", "Lower Potential", "Shrinking Fields", "Winners Pull Away".
entry_barrier_trend string Filter by ai_expertise_impact.entry_barrier_trend: Rising | Falling.
expertise_premium_trend string Filter by ai_expertise_impact.expertise_premium_trend: Rising | Falling.
expand string Comma-separated tokens. expand=ai_impact inlines the full ai_impact block (narratives + trends + flag + coverage) on each row.
sort string Sort field. Aliases: order_by. Sortable: consolidated_title (alias title), onet_code, cluster_id, cluster_name, ai_flag, entry_barrier_trend, expertise_premium_trend, job_level, company_count_*. NULLS LAST is unconditionally applied.
order string asc | desc (default asc). Aliases: dir.
limit int Max results
offset int Pagination

Examples:

GET /api/occupations?ai_flag=Raising%20the%20Bar&limit=10
GET /api/occupations?ai_flag=Lower%20Potential&job_level=Bachelor%27s%20Degree&limit=20
GET /api/occupations?entry_barrier_trend=Rising&expand=ai_impact&limit=5
GET /api/occupations?sort=expertise_premium_trend&order=desc&limit=10

GET /api/occupations/{id}

Get occupation details. The {id} can be: - O*NET code: 15-1252.00 - Cluster ID: cluster:187

Examples:

GET /api/occupations/15-1252.00
GET /api/occupations/cluster:187

Response:

{
  "success": true,
  "data": {
    "onet_code": "15-1252.00",
    "onet_title": "Software Developers",
    "cluster_id": 5,
    "cluster_name": "Computer and Mathematical"
  }
}

GET /api/occupations/{id}/wages

Wage data for an occupation or cluster.

Query Parameters: | Parameter | Type | Description | |-----------|------|-------------| | msa_size | string | Filter by MSA size: "Large" or "Small/Medium" | | years_in | integer | Filter by tenure year: 0–10 (0.17.0-beta) | | experience_level | string | The same tenure value as a string, "0"–"10". "0", "5" and "10" resolve to the rows they always did |

Tenure grain (0.17.0-beta, breaking): rows are keyed on years_in, an integer from 0 through 10 — eleven rows per (cluster, msa_size), where before 0.17.0-beta there were three (0, 5, 10). Rows are ordered by years_in and each row carries it. experience_level is the text mirror of the same value. Clients that indexed wage rows by position must read years_in instead.

wage_growth block (0.17.5-beta): a cluster-level object served beside the rows (beside wages_by_segment on /api/occupations/{onet}/wages, beside data on /api/clusters/{id}/wages, and inside data on GET /api/clusters/{id}). null when no value is published for the cluster.

Field Type Description
p50_growth_0_10 number Median wage growth from year 0 to year 10 (e.g. 0.150226 = 15.0%)
tier string "low", "medium" or "high" — the provider's tertile within one delivery, so relative, not absolute
basis string The weighting, carried in the response
note string How to use it, carried in the response

⚠️ Two different weightings. p50_growth_0_10 is employment-weighted across msa_size by the provider; the per-row percentiles are unweighted within each msa_size. (median at years_in 10 / median at years_in 0) − 1 therefore does not reproduce p50_growth_0_10, and for most clusters the two disagree. Quote one or the other and say which; do not reconcile them.

Examples:

GET /api/occupations/15-1252.00/wages
GET /api/occupations/15-1252.00/wages?msa_size=Large
GET /api/occupations/15-1252.00/wages?msa_size=Large&years_in=7
GET /api/occupations/cluster:187/wages?msa_size=Small/Medium

Single Occupation Response:

{
  "success": true,
  "data": {
    "onet_code": "15-1252.00",
    "wages": {
      "median_annual": 120730,
      "percentile_10": 72530,
      "percentile_90": 168570
    }
  }
}

Cluster Response:

{
  "success": true,
  "data": [...],
  "count": 22,
  "cluster_id": 187,
  "cluster_name": "Software Engineers",
  "wage_growth": { "p50_growth_0_10": 0.150226, "tier": "medium", "basis": "...", "note": "..." }
}

count is 22: eleven tenure years for each of the two MSA sizes.

GET /api/occupations/{id}/ai-impact

AI-impact assessment for an occupation or cluster. The response carries the full narrative + trend surface (ai_flag, both trend fields, both expertise narratives, and the coverage block). Since 0.23.0-beta it also carries ai_impact_scores (see the ai_impact_scores block) and premium_skills_summary. Since 0.17.0-beta it carries premium_skill_details (each premium skill with its description, which is null since 0.23.0-beta).

On the occupation form (/api/occupations/{onet}/ai-impact) the scores block is the occupation's cluster's, plus occupation_scores: this occupation's own row, or null with a note when the cluster score was not computed from it.

Examples:

GET /api/occupations/15-1252.00/ai-impact
GET /api/occupations/cluster:5/ai-impact
GET /api/occupations/cluster:187/ai-impact

The cluster form (cluster:{cluster_id}) is preserved as an alias; new callers should prefer GET /api/clusters/{cluster_id}/ai-impact (below).

Response (classified):

{
  "success": true,
  "data": {
    "onet_code": "13-2011.00",
    "occupation_title": "Accountants and Auditors",
    "ai_impact": {
      "ai_flag": "Raising the Bar",
      "entry_barrier_trend": "Rising",
      "expertise_premium_trend": "Rising",
      "low_expertise_narrative": "...narrative paragraph for low-expertise practitioners...",
      "high_expertise_narrative": "...narrative paragraph for high-expertise practitioners...",
      "coverage": {
        "classified": true,
        "methodology_version": "2026-04-21"
      }
    }
  }
}

When the cluster has no ai_expertise_impact row, the response is still success=true with ai_impact.coverage.classified=false, all narrative + trend + flag fields null, and a coverage.reason string.

GET /api/clusters/{cluster_id}/ai-impact (NEW)

Path-natural cluster-scoped form of the AI-impact endpoint. Same handler as GET /api/occupations/cluster:{cluster_id}/ai-impact. Returns:

  • summary — cluster-level rollup (total_occupations, ai_flag_distribution)
  • ai_impact — always-expanded ai_impact block (narratives + trends + flag + coverage)
  • ai_impact_scores — AI impact scores for the cluster (0.23.0-beta; see below)
  • data — list of occupations in the cluster
  • cluster_id, cluster_name, count

Errors:

  • 400 Bad Request — cluster_id must be an integer
  • 404 Not Found — Cluster not found

Examples:

GET /api/clusters/187/ai-impact     # scores from every occupation in the cluster
GET /api/clusters/94/ai-impact      # scores from 3 of 5 occupations
GET /api/clusters/2/ai-impact       # no published scores

DSL twin: GET AI_IMPACT FOR CLUSTER "187".

The ai_impact_scores block (0.23.0-beta)

Served on GET /api/clusters/{id}/ai-impact and GET /api/occupations/cluster:{id}/ai-impact (top-level key), on GET /api/occupations/{onet}/ai-impact (inside data, with occupation_scores added), and on GET /api/clusters/{id} (inside data). It is separate from the ai_impact classification block, which is unchanged. SUBJECT TO CHANGE.

No published scores (about 625 of 794 clusters):

{"available": false, "message": "AI impact information not available at this time."}

Published scores (GET /api/clusters/94/ai-impact, abridged):

{
  "available": true,
  "demand_score": -3.8,
  "entry_level_score": -0.2,
  "skills_change_score": 1.8,
  "method": "Weighted avg by monthly postings",
  "methodology_version": "2026-10-23",
  "occupations_with_data": [
    {"onet_code": "19-1029.02", "title": "Molecular and Cellular Biologists", "monthly_postings": 1397.5,
     "demand_score": -5.0, "entry_level_score": 0.5, "skills_change_score": 1.1}
  ],
  "occupations_without_data": [
    {"onet_code": "19-1029.00", "title": "Biological Scientists, All Other"},
    {"onet_code": "19-1029.04", "title": "Biologists"}
  ],
  "notes": ["These scores were computed from 3 of 5 occupations in this cluster and do not describe those in occupations_without_data. ..."],
  "scales": {"demand_score": "-5 to +5. ...", "entry_level_score": "-5 to +5. ...", "skills_change_score": "0 to 5. ..."},
  "note": "Scores are continuous and published to one decimal. Report them as published; ..."
}

Field Type Description
available bool false when the cluster has no published scores
message string Only when available is false. The text to show a user
demand_score float −5 to +5
entry_level_score float | null −5 to +5. null where none is published — do not infer one
skills_change_score float | null 0 to 5. null where none is published
method string Direct (single occupation) or Weighted avg by monthly postings
methodology_version string 2026-10-23
occupations_with_data array Occupations the cluster score was computed from, with their own scores and monthly_postings (the weights), largest first
occupations_without_data array Occupations in the cluster that the score does not describe
notes array of string Caveats for this cluster: unpublished scores, partial coverage, and (occupation form) an occupation outside the computed set
occupation_scores object | null Occupation form only: this occupation's own row, or null
scales, note object, string What each scale means, and the instruction to report scores as published

Three kinds of absence. available: false means the provider published no scores for the cluster. A null score inside an available block means that one score was not published. The whole block being null means the read failed — treat it as an error, not as "no data", and retry.

GET /api/occupations/{id}/education

Education requirements for occupation or cluster.

Examples:

GET /api/occupations/15-1252.00/education
GET /api/occupations/cluster:187/education

GET /api/occupations/{id}/pathways

Career pathways for an occupation: destination clusters, retention by experience, skill-adjacent clusters and premium skills.

Response fields (data): onet_code, cluster_id, cluster_name, destination_cluster_ids, destination_clusters, skill_adjacent_clusters, retention_by_experience, premium_skills, premium_skill_details (each skill with a description, null since 0.23.0-beta) and premium_skills_summary (0.23.0-beta) — one paragraph on how the premium skills improve position, covering all of them; null when the cluster has none.

Example:

GET /api/occupations/15-1252.00/pathways

Removed (0.16.7-beta): the cluster form GET /api/occupations/cluster:{id}/pathways returns 410 Gone with a suggestion. Use GET /api/clusters/{id}/cluster-destinations and GET /api/clusters/{id}/skill-adj-clusters instead.

GET /api/occupations/badges (0.12.1-beta)

Cross-employer (company × occupation-cluster) pair-level badge rows. Each row represents one company-and-occupation-cluster combination with its three archetype badges plus identity columns (company_name, cluster_id, cluster_name, entry_posting_count, entry_posting_tier, total_posting_count, total_posting_tier, early_career_access, top10_occupation).

This is the row-level analytic backbone — use it when you want all assessed (company × role) combinations matching some criterion in a single call, instead of iterating per company via /api/companies/{name}/occupations.

Parameters:

Name Type Description
badge_early_career string Platinum, Gold, or Ranked (any non-null tier). Comma-separated for OR.
badge_growth string Same vocabulary as badge_early_career.
badge_stability string Same vocabulary.
company string Exact company name (case-insensitive).
cluster_id int Occupation cluster ID (lazily JOINs occupation_cluster only if used).
onet_code string O*NET-SOC code (lazily JOINs occupation_cluster only if used).
industry string Exact industry name (lazily JOINs companies only if used).
entry_posting_tier string Entry-level hiring tier on the pair: high/above-average/average/low/none.
total_posting_tier string Total hiring tier on the pair (same vocabulary).
top10 string 1 to restrict to the company's top-10 most prevalent occupations (0 for non-top-10).
limit int Max results (default 100, max 1000).
offset int Pagination offset (default 0).

Examples:

# All Platinum-growth (company × occupation) pairs (1,000-row cap)
GET /api/occupations/badges?badge_growth=Platinum&limit=1000

# Amazon's Platinum-stability pairs in one call
GET /api/occupations/badges?company=Amazon&badge_stability=Platinum&limit=200

# High-volume Healthcare growth pairs with any non-null growth tier
GET /api/occupations/badges?industry=Healthcare&badge_growth=Ranked&total_posting_tier=high&limit=200

# All pairs in cluster 187 with any-tier early-career badge
GET /api/occupations/badges?cluster_id=187&badge_early_career=Ranked

Error handling: A non-integer cluster_id returns HTTP 400 with a clear message before reaching the database (e.g. ?cluster_id=abc → {"success": false, "error": "cluster_id must be an integer"}).

GET /api/occupations/badges/counts (0.12.1-beta)

Scalar counts and per-group rollups over /api/occupations/badges. Accepts the same filter params as the list endpoint, plus group_by for grouped output.

Parameters (in addition to all /api/occupations/badges filter params):

Name Type Description
group_by string One of archetype, company, industry, cluster_id. Omit for a scalar total.

group_by=archetype returns the 3×3 matrix across ALL ~38,500 assessed pairs:

{
  "success": true,
  "data": {
    "group_by": "ARCHETYPE",
    "by_archetype": {
      "early_career": { "platinum":  6202, "gold":  4562, "ranked": 10764 },
      "growth":       { "platinum": 13022, "gold": 10274, "ranked": 23296 },
      "stability":    { "platinum":  7493, "gold":  7555, "ranked": 15048 }
    },
    "total_rows": 38481
  }
}

Examples:

# Per-company tally of Platinum-growth pairs (which employers concentrate them?)
GET /api/occupations/badges/counts?badge_growth=Platinum&group_by=company

# Per-cluster Platinum-stability density
GET /api/occupations/badges/counts?badge_stability=Platinum&group_by=cluster_id

# Healthcare's full 3×3 matrix (forces companies-table JOIN)
GET /api/occupations/badges/counts?industry=Healthcare&group_by=archetype

Error handling: Unknown group_by values return HTTP 400 with the structured error body preserved under data.error (including valid_group_by, suggestion, example).

GET /api/occupations/{id}/companies

List companies that employ an occupation. {id} is an O*NET code for this endpoint.

Parameters:

Name Type Description
badge string Filter by badge level (Platinum, Gold) — matches across all three archetype columns.
badge_early_career string Filter by early-career badge on the (company × this-occupation) pair. Values: Platinum, Gold, or Ranked (any non-null tier).
badge_growth string Filter by growth badge on the pair. Same value vocabulary.
badge_stability string Filter by stability badge on the pair. Same value vocabulary.
entry_posting_tier string Filter by entry-level hiring tier (high/above-average/average/low/none).
total_posting_tier string Filter by total hiring tier (same vocabulary).
sector string Filter by company industry / sector.
limit int Max results (default 100).
offset int Pagination offset (default 0).

Ranked sentinel — passing Ranked on any badge_* field matches companies whose pair-level badge on that archetype is non-null (any of Platinum / Gold). Honored on this endpoint as of 0.12.3-beta — earlier releases returned 0 rows for this query.

DEPRECATION (6-2-26): postings_qtile was retired — ?postings_qtile= and ?sort=postings_qtile return HTTP 400 with a deprecation error. Use the tier filters and ?sort=entry_postings|total_postings (or DSL ORDER BY ENTRY_POSTING_COUNT|TOTAL_POSTING_COUNT). SUBJECT TO CHANGE.

Examples:

GET /api/occupations/15-1252.00/companies?limit=10
GET /api/occupations/15-1252.00/companies?badge=Platinum&limit=10
# Any-tier growth on this role (Ranked sentinel — 0.12.3-beta)
GET /api/occupations/15-1252.00/companies?badge_growth=Ranked&limit=25
# High-volume Healthcare hirers with Platinum stability on this role
GET /api/occupations/15-1252.00/companies?badge_stability=Platinum&sector=Healthcare&total_posting_tier=high

GET /api/occupations/{id}/clusters

List cluster membership for an occupation. {id} is an O*NET code for this endpoint.

Example:

GET /api/occupations/15-1252.00/clusters


Cluster Endpoints

GET /api/clusters

List occupation clusters with expanded career info (job level, skills, adjacent clusters).

Parameters:

Name Type Description
search string Filter by cluster name
cluster_id integer Filter by specific cluster ID
job_level string Filter by education level
premium_skill string Filter by skill (partial match)
ai_flag string Filter by AI impact category (exact match). Values: "Raising the Bar", "Lower Potential", "Shrinking Fields", "Winners Pull Away".
entry_barrier_trend string Filter by ai_expertise_impact.entry_barrier_trend: Rising | Falling.
expertise_premium_trend string Filter by ai_expertise_impact.expertise_premium_trend: Rising | Falling.
expand string Comma-separated tokens. expand=ai_impact inlines the full ai_impact block on each cluster row.
sort string Sort field. Aliases: order_by. Sortable: cluster_name, cluster_id, ai_flag, entry_barrier_trend, expertise_premium_trend, occupation_count, ba_plus_share. NULLS LAST is unconditionally applied.
order string asc | desc (default asc). Aliases: dir.
include string Set to occupations to include occupation list
limit int Max results (default: 100)

Job Level Values: - Advanced Degree - Bachelor's Degree - Bachelor's Degree + Experience - Sub-Bachelor's Degree - Sub-Bachelor's Degree + Experience

Examples:

GET /api/clusters?limit=10
GET /api/clusters?job_level=Advanced%20Degree&limit=10
GET /api/clusters?premium_skill=Cloud&limit=10
GET /api/clusters?entry_barrier_trend=Rising&expand=ai_impact&limit=5
GET /api/clusters?sort=expertise_premium_trend&order=desc&limit=10

Response:

{
  "success": true,
  "data": [
    {
      "cluster_id": 1,
      "cluster_name": "Accountants and Auditors",
      "cluster_occs": "Accountants and Auditors",
      "occupation_count": 1,
      "job_level": "Bachelor's Degree",
      "skill_adj_cluster_1": 980,
      "skill_adj_cluster_2": 355,
      "skill_adj_cluster_3": 105,
      "premium_skill_1": "Auditing",
      "premium_skill_2": "Specialized Accounting",
      "premium_skill_3": "Internal Controls"
    }
  ]
}

Numbered fields are not fixed at three. skill_adj_cluster_N, premium_skill_N and the destination fields are published at whatever width the provider delivers (destinations moved from 3 to 5 in 0.17.0-beta). Iterate the keys present rather than hardcoding _1/_2/_3. The premium_skill filter matches every slot.

GET /api/clusters/{id}

Get cluster details with expanded info: description, skills, education share, wage growth and AI impact.

Examples:

GET /api/clusters/187
GET /api/clusters/187?include=occupations

Response Fields:

Field Type Description
cluster_id int Cluster ID
cluster_name string Cluster name
cluster_description string What the work is, one or two sentences (0.23.0-beta)
cluster_occs string Pipe-separated list of occupations
occupation_count int Number of O*NET codes in cluster
job_level string Education/experience level category
common_clean_job_title_N string Common job titles in the cluster
skill_adj_cluster_N int IDs of skill-adjacent clusters
premium_skill_N string Skills that carry a wage premium
premium_skill_description_N null Kept for compatibility; always null since 0.23.0-beta. Use premium_skills_summary
premium_skills_summary string | null How the premium skills improve position, one paragraph covering all of them. null when the cluster has no premium skills (0.23.0-beta)
ba_plus_share float Share with a bachelor's degree or higher
ai_flag string | null Deprecated AI-impact label — see Methodology
wage_growth object | null See the wages endpoint (0.17.5-beta)
ai_impact_scores object See the ai_impact_scores block (0.23.0-beta)

Response (abridged):

{
  "success": true,
  "data": {
    "cluster_id": 187,
    "cluster_name": "Software Engineers",
    "cluster_description": "Design, write, test, and update the code behind computer applications, websites, and systems, ...",
    "premium_skill_1": "Generative Models",
    "premium_skill_2": "Kotlin",
    "premium_skill_3": "Back End (Software Engineering)",
    "premium_skill_description_1": null,
    "premium_skills_summary": "With Generative Models the engineer gives the product a feature that produces text, images or code for its users. ...",
    "wage_growth": { "p50_growth_0_10": 0.150226, "tier": "medium", "basis": "...", "note": "..." },
    "ai_impact_scores": {
      "available": true,
      "demand_score": -2.9,
      "entry_level_score": -1.3,
      "skills_change_score": 3.4,
      "method": "Weighted avg by monthly postings",
      "methodology_version": "2026-10-23",
      "occupations_with_data": [ "... 2 rows ..." ],
      "occupations_without_data": [],
      "notes": [],
      "scales": { "...": "..." },
      "note": "..."
    }
  }
}

GET /api/clusters/{id}/companies

List companies that hire for an occupation cluster, with optional badge, industry, geographic (CBSA), and hiring-intensity filters, plus server-side sort modes for the most common visualization patterns ("top hirers", "top employers by badge", "group by industry"). Each row includes the company's primary_industry (added 0.12.4-beta). Server-side sort is whitelisted; passing an unknown value returns HTTP 400 with the valid set.

Parameters:

Name Type Description
entry_posting_tier string Occupation-level entry hiring tier on company_occupation_summary (high/above-average/average/low/none).
total_posting_tier string Occupation-level total hiring tier (same vocabulary).
badge string Filter by badge value (e.g. Platinum, or growth:Gold for a specific column).
badge_early_career string Filter by early-career badge. Values: Platinum, Gold, or Ranked (any non-null tier on this archetype).
badge_growth string Filter by growth badge. Same value vocabulary as badge_early_career.
badge_stability string Filter by stability badge. Same value vocabulary as badge_early_career.
industry string Filter by primary industry.
top10 string Filter by top-10 occupation flag (1/0).
cbsa string Companies hiring for this cluster in this metro. Accepts a numeric CBSA code (e.g. 35620 for NYC) or a metro name (matched against cbsa_name, e.g. Boston-Cambridge-Newton, MA-NH).
hiring_postings_qtile string Geographic hiring intensity from hiring_flag.postings_count_qtile (1–3 quantile). A different column from the occupation-level posting tiers on company_occupation_summary. Literal alias: hiring_postings_count_qtile. Deprecated alias: hiring_percentile — still works but emits deprecation_warning in the response envelope; migrate to hiring_postings_qtile.
sort string One of alpha (default), total_postings (total_posting_count DESC), entry_postings (entry_posting_count DESC), badge, industry (sorts on primary_industry, A→Z, companies without an industry last; added 0.12.4-beta), or wywm (calculated badge×hiring rank — see below). Alias: order_by.
archetype string Only used with sort=wywm. Which badge archetype the WYWM rank reads: early_career (default), growth, or stability. May also ride on the sort token (sort=wywm_growth). Unknown values fall back to early_career.
include_score bool When true (or implicit with sort=badge), exposes badge_sort_score on each row.
limit int Max results (default 100, max 1000).
offset int Pagination offset (default 0).

DEPRECATION (6-2-26 — SUBJECT TO CHANGE): the old postings_qtile filter and the postings_qtile sort mode were retired. Sending ?postings_qtile= or ?sort=postings_qtile returns HTTP 400 with a structured deprecation error. Use entry_posting_tier / total_posting_tier (filter) and sort=entry_postings / sort=total_postings (sort).

WYWM sort (sort=wywm, updated 0.13.0-beta — SUBJECT TO CHANGE): a calculated sort that blends the per-pair archetype badge tier (Platinum > Gold) with the hiring tier (high > above-average > average > low > none). The hiring column is archetype-dependent: early_career uses entry_posting_tier; growth/stability use total_posting_tier. Rows are ranked in this fixed priority order; rows whose chosen badge is null/NA or whose tier is missing get rank 99 and sort last (then A→Z):

Rank Badge Hiring tier
1 Platinum high
2 Platinum above-average
3 Gold high
4 Gold above-average
5 Platinum average
6 Platinum low
7 Gold average
8 Gold low
9 Platinum none
10 Gold none

The archetype parameter (default early_career) selects which badge supplies the tier (and which posting-tier column is read). Each row carries wywm_rank (1=best … 99=unranked); the response envelope echoes wywm_archetype. The exact bucket order may be tuned during the beta.

Occupation-level tiers vs geographic quantile: entry_posting_tier/total_posting_tier are textual tiers (high…none) on company_occupation_summary; hiring_postings_qtile is a 1–3 numeric quantile on hiring_flag (the hiring_ prefix denotes the table scope).

Examples:

GET /api/clusters/42/companies
GET /api/clusters/42/companies?total_posting_tier=high
GET /api/clusters/42/companies?badge=Platinum&industry=Software%20%26%20Technology
GET /api/clusters/187/companies?cbsa=35620&limit=25
GET /api/clusters/187/companies?cbsa=Boston-Cambridge-Newton%2C%20MA-NH
GET /api/clusters/187/companies?hiring_postings_qtile=3
GET /api/clusters/42/companies?sort=total_postings&limit=25
GET /api/clusters/42/companies?sort=entry_postings&limit=25
GET /api/clusters/42/companies?sort=badge&include_score=true&limit=25
GET /api/clusters/42/companies?sort=industry&limit=25

# WYWM calculated rank (Platinum/Gold × hiring tier; default early_career archetype → entry_posting_tier)
GET /api/clusters/42/companies?sort=wywm&limit=25
GET /api/clusters/42/companies?sort=wywm&archetype=growth&limit=25
GET /api/clusters/42/companies?sort=wywm_growth&limit=25

# Any-tier filtering with the Ranked sentinel — fixed in 0.12.3-beta
# (earlier releases returned 0 rows for these queries because the value was
# treated as a literal string match against a column whose values are
# Platinum/Gold/null/NA, never the sentinel itself).
GET /api/clusters/187/companies?badge_growth=Ranked&limit=25
GET /api/clusters/187/companies?badge_early_career=Ranked&badge_stability=Ranked&cbsa=35620

# Deprecated alias still works but emits deprecation_warning:
GET /api/clusters/187/companies?hiring_percentile=3

Response (with sort=badge&include_score=true):

{
  "status": "success",
  "data": [
    {
      "company_name": "Amazon",
      "cluster_name": "Software Engineers",
      "primary_industry": "Retail & E-Commerce",
      "badge_early_career": "Platinum",
      "badge_growth": "Platinum",
      "badge_stability": "Platinum",
      "entry_posting_count": 1609,
      "entry_posting_tier": "high",
      "total_posting_count": 1947,
      "total_posting_tier": "high",
      "early_career_access": 1,
      "top10_occupation": "1",
      "badge_sort_score": 45.0
    }
  ],
  "count": 1,
  "total": 1084,
  "cluster_id": 42,
  "sort": "badge"
}

Response (with sort=wywm — note wywm_rank per row and the echoed wywm_archetype):

{
  "status": "success",
  "data": [
    {
      "company_name": "Amazon",
      "cluster_name": "Software Engineers",
      "primary_industry": "Retail & E-Commerce",
      "badge_early_career": "Platinum",
      "badge_growth": "Platinum",
      "badge_stability": "Gold",
      "entry_posting_count": 1609,
      "entry_posting_tier": "high",
      "total_posting_count": 1947,
      "total_posting_tier": "high",
      "early_career_access": 1,
      "top10_occupation": "1",
      "wywm_rank": 1
    }
  ],
  "count": 1,
  "total": 1084,
  "cluster_id": 42,
  "sort": "wywm",
  "wywm_archetype": "early_career"
}

Invalid sort error (HTTP 400):

{
  "success": false,
  "error": "Invalid sort mode: 'bogus'",
  "suggestion": "Use one of: alpha, entry_postings, total_postings, badge, industry, wywm",
  "example": "LIST COMPANIES FOR CLUSTER \"42\" ORDER BY TOTAL_POSTING_COUNT DESC LIMIT 25",
  "valid_sort_modes": ["alpha", "entry_postings", "total_postings", "badge", "industry", "wywm"]
}

GET /api/clusters/{id}/skill-adj-clusters (NEW)

Clusters that share the skill profile of a baseline cluster. Filters and metric thresholds reduce the set further.

The neighbour set is curated, and complete as published (0.16.11-beta). The provider selects a representative set of neighbours per cluster and publishes no long tail, so there is no fuller list behind the response. Do not describe it as a sample, top-N or truncated list. A cluster that is absent was not selected, which is not evidence that it is unrelated. Read count / total rather than assuming a width: skill adjacency publishes up to 3, destinations up to 5 (0.17.0-beta).

data_status (0.16.8-beta). Every row and the baseline object carry data_status: "attributed" (the cluster has wages, skills and metrics) or "taxonomy_only" (it exists and titles resolve to it, but no attributes are published, e.g. military occupations or Chief Executives). A taxonomy_only baseline returns 200 with data: [] and a data_status_note, not an error; an unregistered cluster_id still returns an error.

Use cluster id=0 and ?onet=<code> to baseline by O*NET code instead of cluster ID. The path segment is then ignored.

Parameters:

Name Type Default Description
onet string — O*NET code as the baseline (overrides path {id} when set)
company string — Restrict to clusters this company hires for (exact, case-insensitive). Required when expand=company_hiring.
job_level string — Restrict to clusters at this education tier
ai_flag string — Restrict to clusters with this AI-impact category
premium_skill string — Substring match on every premium_skill_N slot
experience_level string "10" Snapshot key. Two key spaces exist: wage_data uses years ("0"–"10"); promotion_retention uses bands ("Entry-Level", "Experienced"). The default "10" populates wage_p75_at_10y from wage_data but does not match promotion_retention rows, so the three promotion fields will be null. To populate promotion metrics, pass Entry-Level or Experienced.
msa_size string "Large" Snapshot key. Values: "Large", "Small/Medium" (the smaller bucket is one combined value, not two).
min_internal_promotion_rate float — Lower bound on metrics.internal_promotion_rate (inclusive)
max_internal_promotion_rate float — Upper bound (inclusive)
min_external_promotion_rate float — Lower bound (inclusive)
max_external_promotion_rate float — Upper bound (inclusive)
min_retention_rate_3yr float — Lower bound (inclusive)
max_retention_rate_3yr float — Upper bound (inclusive)
min_wage_p75_at_10y float — Lower bound (inclusive)
max_wage_p75_at_10y float — Upper bound (inclusive)
expand string — Comma-separated. Valid: names, occupations, company_hiring
limit int 100 Max rows (1–1000)
offset int 0 Pagination offset

Null behavior on metric filters. When min_* / max_* is set, rows whose metric value is null at the chosen (experience_level, msa_size) snapshot are excluded — "no data" is treated as "doesn't satisfy the threshold". With no metric filter active, all rows are returned and any missing metric value comes back as null.

Examples:

# All skill-adjacent clusters at the default snapshot
GET /api/clusters/187/skill-adj-clusters?expand=names

# Skill-adjacent clusters that pay better and retain better than typical
GET /api/clusters/187/skill-adj-clusters?min_wage_p75_at_10y=125000&min_retention_rate_3yr=0.65&expand=names

# Same, but baseline by O*NET code
GET /api/clusters/0/skill-adj-clusters?onet=15-1252.00&min_wage_p75_at_10y=125000&expand=names

# Adjacency restricted to a company's hiring footprint
GET /api/clusters/187/skill-adj-clusters?company=Amazon&expand=names,company_hiring

Response:

{
  "status": "success",
  "subject": "skill_adjacent_clusters",
  "baseline": { "cluster_id": 187, "cluster_name": "Software Engineers", "data_status": "attributed" },
  "snapshot": { "experience_level": "10", "msa_size": "Large" },
  "filters": { "wage_p75_at_10y__gte": 125000.0, "retention_rate_3yr__gte": 0.65 },
  "expand": ["NAMES"],
  "data": [
    {
      "cluster_id": 242,
      "cluster_name": "Data Scientists",
      "data_status": "attributed",
      "job_level": "Bachelor's Degree",
      "ai_flag": "Winners Pull Away",
      "premium_skill_1": "Python",
      "premium_skill_2": "Machine Learning",
      "premium_skill_3": null,
      "metrics": {
        "internal_promotion_rate": 0.18,
        "external_promotion_rate": 0.12,
        "retention_rate_3yr": 0.72,
        "wage_p75_at_10y": 142000.0
      }
    }
  ],
  "count": 1,
  "total": 1
}

Error — non-numeric metric threshold (HTTP 400):

{
  "success": false,
  "error": "min_wage_p75_at_10y must be numeric (got: 'a lot')"
}

Error — retired expand=metrics (HTTP 400):

{
  "success": false,
  "error": "Unknown EXPAND target(s): METRICS",
  "suggestion": "Valid targets: COMPANY_HIRING, NAMES, OCCUPATIONS",
  "data": "EXPAND METRICS was retired. Each row now carries a `metrics` block by default; use WHERE EXPERIENCE_LEVEL IS \"<level>\" AND MSA_SIZE IS \"<size>\" to change the snapshot."
}

GET /api/clusters/{id}/cluster-destinations (NEW)

Clusters people in the baseline cluster typically transition into. Same parameters and response shape as /skill-adj-clusters above — the only difference is the underlying source columns (cluster_id_destination_N, up to 5 since 0.17.0-beta, instead of skill_adj_cluster_N) and the response subject: "destination_clusters".

Examples:

# Destinations at the default snapshot
GET /api/clusters/187/cluster-destinations?expand=names

# Destinations at the bachelor's tier with promotion-rate threshold
GET /api/clusters/187/cluster-destinations?job_level=Bachelor%27s%20Degree&min_internal_promotion_rate=0.15&expand=names

# Destinations for an O*NET-baselined query, with combined metric thresholds
GET /api/clusters/0/cluster-destinations?onet=15-1252.00&min_wage_p75_at_10y=140000&max_wage_p75_at_10y=200000&expand=names,occupations

Response shape is identical to /skill-adj-clusters with "subject": "destination_clusters".


Location Endpoints

GET /api/location/zip/{zip}

Get location data by ZIP code.

Example:

GET /api/location/zip/94102

Response:

{
  "success": true,
  "data": {
    "zip_code": "94102",
    "city": "San Francisco",
    "state": "CA",
    "cbsa_code": "41860",
    "cbsa_name": "San Francisco-Oakland-Berkeley, CA"
  }
}

GET /api/location/cbsa/{cbsa}

Get location data by CBSA code.

Example:

GET /api/location/cbsa/41860


Search Tools

GET /api/titleconvert

Convert job title to ONET code(s) with intelligent disambiguation*.

Parameters:

Name Type Default Description
title string (required) Job title to convert
company string - Company context (recommended - 95% accuracy)
industry string - Industry hint (e.g., "fast food", "healthcare") - 85% accuracy
sector string - Company sector filter — a hard filter on the company primary_industry (distinct from the softer industry disambiguation hint; when both are given industry falls back to sector).
location string - City/MSA for regional patterns - 70% accuracy
min_score float 0.5 Min confidence (0-1). See the note below — on the current default paths (fusion + fast typeahead) this 0.5 is not applied as a hard cutoff.
limit int 10 Max results
group string - Set to onet for unique O*NET codes
expand string - cluster attaches the occupation cluster (cluster_id + canonical cluster_name) to each result, resolving title → ONET → cluster in one call. Opt-in (omitted unless requested). SUBJECT TO CHANGE.*
ambiguity_threshold float 0.3 Score gap threshold for disambiguation
clarify boolean false Include clarification options
_fast boolean (server default ON) Fast typeahead mode on the no-company path — word-prefix over the survey NAME corpus; returns distinct occupations (match_source=name_corpus). Set false to force the legacy pipeline. SUBJECT TO CHANGE.
_catalog, _fusion, _bypass, _fts_and, _stem boolean server default Experimental matcher A/B toggles. WILL CHANGE.
_fusion_alpha, _fusion_beta, _fusion_exact, _exact_bonus float server default Experimental fusion-scorer weights + exact-surface match bonus (stemmed FTS path). WILL CHANGE.
_fusion_lex_mode string server default Experimental lexical-calibration mode. WILL CHANGE.

min_score on the default paths (0.14.5-beta+). Fusion ranking is default ON, and when it is active the legacy min_score=0.5 hard cutoff is not applied — gating moves to a low fused floor so sibling occupations under a dominant match survive. On the no-company fast typeahead path (match_source=name_corpus, default ON) there is no min_score gate at all. Treat min_score as effective only when you force the legacy pipeline (_fusion=false and/or _fast=false). Combined with the 0.15.2-beta 1.0-anchoring (see Changes note), a fixed client-side min_score may filter differently than before.

Accuracy Tips: - Ambiguous titles like "manager", "analyst" may return multiple ONET codes - Adding company dramatically improves accuracy - Adding industry helps when company is unknown - No-company results are de-duplicated by cluster (0.16.1-beta; by ONET code before that), so limit counts distinct clusters — two ONET codes in one cluster no longer appear as two rows offering the same answer. group=onet is unaffected. - Boolean query parameters (_fast, _catalog, clarify, ...) accept 1/true/yes/on (0.16.1-beta). Previously only the literal true worked. - Show display_name (0.17.7-beta) — see Response Fields. - Bulk / batch conversion is DSL/MCP-only* — REST is single-title. Use TITLECONVERT ["a","b",...] via /api/dsl or the aoi_dsl_query MCP tool to resolve up to 200 titles in one call.

Example (basic):

GET /api/titleconvert?title=software%20engineer&limit=5

Example (with context):

GET /api/titleconvert?title=manager&company=McDonalds&industry=fast%20food

Example (with cluster):

GET /api/titleconvert?title=software%20engineer&expand=cluster

Example (unique occupations — autocomplete):

GET /api/titleconvert?title=nurse&group=onet&limit=5

Example (ask for clarification on an ambiguous title):

GET /api/titleconvert?title=manager&clarify=true

Example (force the legacy pipeline — company-scoped):

GET /api/titleconvert?title=style%20guru&company=Great%20Clips&_fast=false

Response:

{
  "success": true,
  "data": [
    {
      "onet_code": "15-1252.00",
      "onet_title": "Software Developers",
      "display_name": "Software Developers",
      "display_source": "onet_title",
      "match_score": 1.0,
      "confidence": "high",
      "match_source": "name_corpus+catalog_exact"
    },
    {
      "onet_code": "15-1253.00",
      "onet_title": "Software Quality Assurance Analysts and Testers",
      "display_name": "Software Quality Assurance Analysts and Testers",
      "display_source": "onet_title",
      "match_score": 0.0443,
      "confidence": "low",
      "match_source": "name_corpus"
    }
  ],
  "disambiguation": {
    "status": "resolved",
    "signals_used": ["historical_pattern"]
  }
}

Response Fields: | Field | Description | |-------|-------------| | onet_frequency | How many companies have this occupation (popularity). Company-scoped / legacy path only | | match_score | Relevance score. 1.0-anchored as of 0.15.2-beta on the database/fusion path (best match = 1.0, others relative); the fast typeahead path was already 1.0-anchored. | | display_name | The label to show a person (0.17.7-beta): the cluster name, ONET title or a provider suboccupation name — whichever answers the typed text most directly. The cluster name wins ties. Render display_name \|\| cluster_name \|\| onet_title. cluster_name (with expand=cluster) still identifies the answer to every other endpoint. | | display_source | Which name display_name is: cluster_name, onet_title or bgi_suboccupation. | | confidence | high or low on the catalog path (0.16.2-beta), reflecting whether independent sources agree on the answer; other values may appear on other paths. Prefer it to match_score for deciding whether to ask the user to confirm. | | match_source | Which retrievers found the row. Since 0.16.2-beta it may be a +-joined union (e.g. name_corpus+catalog_exact, database+vector). Test membership, not equality.* hint marks a curated exact mapping. | | recommended | true if this is the system's best recommendation. Company-scoped / legacy path only | | owner_class | Present and true only for self-employment-ambiguous titles (e.g. owner) — a hint to supply company/industry rather than trusting the single answer. Omitted otherwise. | | disambiguation.status | resolved (clear match) or ambiguous (multiple possibilities) | | disambiguation.signals_used | What methods were used: historical_pattern, llm_inference |

Best Practice: Always provide company or industry for ambiguous titles. Use group=onet for autocomplete scenarios.

POST /api/titleconvert

Record which occupation the user chose from a prior GET /api/titleconvert call. Do not re-run TITLECONVERT. Copy the GET JSON, set picked to the row they tapped, and POST it. This is a write — POST only (GET is cached at the proxy).

Body (piggyback the GET response):

{
  "query_info": { "title": "warehouse" },
  "picked": {
    "onet_code": "41-2031.00",
    "onet_title": "Retail Salespersons",
    "match_score": 0.87
  }
}

Required: query_info.title (or top-level title) and picked.onet_code. picked is one element of the GET data[] array, copied as-is.

Response: { "success": true, "data": { "id", "title", "onet_code", "recorded" }, "count": 1 }

Intake only — stored for learning, not yet used in ranking.

DSL twins: SUBMIT TITLECONVERT TITLE "warehouse" CHOICE "41-2031.00" or SUBMIT TITLECONVERT VALUE '{...same JSON as this POST...}' (quote JSON with single quotes).

GET /api/companyconvert

Fuzzy match company names. Uses a tiered matching strategy: prefix index, Elasticsearch completion suggester, full search.

Parameters:

Name Type Description
name string Company name (required)
industry string Industry filter
min_score float Min confidence (0-1, default: 0.6)
limit int Max results (default: 10)

Examples:

GET /api/companyconvert?name=Amazn
GET /api/companyconvert?name=micro&limit=5
GET /api/companyconvert?name=apple&industry=Software%20%26%20Technology

Response:

{
  "success": true,
  "data": [
    {
      "company_name": "Amazon",
      "company_uid": "AN85",
      "match_score": 0.92,
      "match_type": "fuzzy",
      "confidence": "high",
      "source": "elasticsearch"
    }
  ],
  "count": 1
}

Response Fields:

Field Type Values Description
match_type string prefix, suggest, fuzzy, exact How the match was found
confidence string high, medium, low Score-derived confidence band
source string prefix_index, es_completion, elasticsearch Which matching tier produced the result

GET /api/clusters/convert

Find occupation clusters by common job title. Searches against the common_clean_job_title columns in occupation data.

Parameters:

Name Type Description
title string Job title to search (required)
job_level string Education level filter (e.g., "Bachelor's Degree")
ai_flag string AI impact filter (e.g., "Raising the Bar")
min_score float Min relevance score (default: 0.3)
limit int Max results (default: 10)

Examples:

GET /api/clusters/convert?title=accountant&limit=5
GET /api/clusters/convert?title=nurse&job_level=Sub-Bachelor%27s%20Degree
GET /api/clusters/convert?title=analyst&ai_flag=Raising%20the%20Bar&limit=3

Response:

{
  "success": true,
  "data": [
    {
      "cluster_id": 1,
      "cluster_name": "Accountants and Auditors",
      "matched_title": "Accountant",
      "matched_field": "common_clean_job_title_1",
      "job_level": "Bachelor's Degree",
      "ai_flag": "Raising the Bar",
      "score": 0.85
    }
  ],
  "count": 1
}


Error Handling

HTTP Status Codes

Code Meaning
200 Success
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
429 Rate Limit
500 Server Error

Error Response

REST endpoints return a flat error envelope:

{
  "success": false,
  "error": "Brief description of what went wrong",
  "suggestion": "How to fix it",
  "example": "Corrected request (when applicable)",
  "data": { "unknown_fields": ["..."], "valid_fields": ["..."] }
}

The example, data, and suggestion fields are populated when the server can offer concrete recovery hints (e.g., unknown filter parameters list valid alternatives). They may be omitted when no specific hint is available.


Rate Limits

Tier Requests/Minute
Standard 60
Premium 500

Best Practices

  1. Cache tokens - Reuse access tokens until expiry
  2. Use pagination - Set limit and offset for large results
  3. Use search tools - titleconvert and companyconvert for fuzzy matching
  4. Handle errors - Check status codes and error messages

Data Field Reference

Company Fields

Field Type Description
company_uid string Unique company identifier (e.g., AN85)
company_name string Official company name
company_description string Company description
company_URL string Company website
primary_industry string Industry classification
secondary_industry string Secondary industry (if applicable)
company_city string Headquarters city
company_state string Headquarters state
company_country string Headquarters country
overall_badge string Overall opportunity badge
badge_early_career string Early career opportunity badge
badge_growth string Growth opportunity badge
badge_stability string Stability badge
company_event_type string Corporate event (e.g., acquired, merged, rebranded) — omitted when none
company_event_note string Event details — omitted when empty
aoi_parents array Parent company UID(s) — empty array [] when none
aoi_subsidiaries array Subsidiary company UID(s) — empty array [] when none
aoi_siblings array Sibling company UID(s) — empty array [] when none
related_companies object Resolved names (only with ?expand=relationships)

Occupation Fields

Field Type Description
onet_code string O*NET-SOC code (XX-XXXX.XX)
onet_title string Standardized O*NET job title
cluster_id int Occupation cluster ID
cluster_name string Cluster name
occupation_count int Number of occupations (in cluster responses)

Cluster Fields

Field Type Description
cluster_id int Cluster identifier
cluster_name string Cluster name
occupation_count int Number of O*NET codes in this cluster
cluster_description string What the work is, one or two sentences (0.23.0-beta)
premium_skill_N string | null Skills that carry a wage premium; width set by the provider
premium_skill_description_N null Always null since 0.23.0-beta
premium_skills_summary string | null One paragraph on how the premium skills improve position (0.23.0-beta)
ba_plus_share float Share of practitioners with a bachelor's degree or higher (where present)
wage_growth object | null {p50_growth_0_10, tier, basis, note} (0.17.5-beta)
ai_flag string | null Deprecated AI-impact label — see Methodology
entry_barrier_trend string | null Rising | Falling
expertise_premium_trend string | null Rising | Falling
ai_impact_scores object AI impact scores (0.23.0-beta) — see the block
data_status string Adjacency responses: attributed | taxonomy_only (0.16.8-beta)

Wage Fields

Field Type Description
years_in int Tenure year, 0–10 (0.17.0-beta)
experience_level string Text mirror of years_in
msa_size string Large | Small/Medium
percentile_25, median, percentile_75 number Annual wage at that tenure and metro size, unweighted within the metro size

AI Impact Fields

Returned by /api/occupations/{id}/ai-impact, /api/clusters/{cluster_id}/ai-impact, and (when expand=ai_impact) inline on /api/occupations and /api/clusters rows.

Field Type Description
ai_flag string | null One of the four AI-impact labels
entry_barrier_trend string | null Rising | Falling
expertise_premium_trend string | null Rising | Falling
low_expertise_narrative string | null Practitioner-facing guidance for early-career / lower-expertise workers in the cluster
high_expertise_narrative string | null Practitioner-facing guidance for experienced / higher-expertise workers
coverage object { classified: bool, methodology_version: string \| null, reason: string? }. When classified: false, all fields above are null.
premium_skill_details array Each premium skill with its description (null since 0.23.0-beta)
premium_skills_summary string | null One paragraph covering all premium skills (0.23.0-beta)
ai_impact_scores object AI impact scores (0.23.0-beta) — see the block. Fields: available, message, demand_score, entry_level_score, skills_change_score, method, methodology_version, occupations_with_data, occupations_without_data, notes, occupation_scores (occupation form), scales, note

Badge value vocabulary is documented under Methodology. Both company-level and (company × occupation) badges use Platinum | Gold | null.


AOI REST API — © 2026 American Opportunity Index