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.15.16-beta
Base URL: https://aonav.ai
Protocol: HTTPS
Authentication: JWT Bearer token
Last Updated: 2026-08-18

Changes since 0.11.7-beta (last external doc revision): - 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 (0.14.2-beta, breaking for MCP clients): the MCP retrieval surface is now a single tool, aoi_dsl_query. The former aoi_title_convert / aoi_company_convert tools were removed — 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 1,016 unique O*NET-SOC codes
Company × occupation pairs assessed ~55,000
Occupation clusters (WYWM cohort grouping) 561
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 AI-impact overlay (below) uses a denser cluster grouping than the core badge methodology; expect 561 in core endpoints and a larger denominator in AI-impact responses.

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 flag

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 26 clusters
Winners Pull Away Falling Rising 23 clusters
Lower Potential Falling Falling 65 clusters

Coverage caveat. 177 of 861 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.

The AI-impact overlay is a separate Burning Glass Institute research project that builds on (is a sequel to) the WYWM core methodology. Treat AI flags as a distinct overlay with their own methodology version stamp and their own coverage profile.

Wage data

Wage tables are returned per occupation, with breakdowns by experience level and metro size (MSA_SIZE: Large, Medium, Small, None). The per-occupation wage table is provisional and 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; field names and response shape may evolve
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. Within the 0.12.x series, one scoped breaking change was made (0.12.2-beta): the derived education_level field was removed from the two education endpoints — see Changes note at the top of this document for the migration path.


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 Developers and Computer Programmers",
      "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 Developers and Computer Programmers",
      "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" | | experience_level | string | Filter by experience: "0", "5", or "10" years |

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&experience_level=5
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": 12,
  "cluster_id": 187,
  "cluster_name": "Software Development"
}

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

AI-impact assessment for an occupation or cluster. The response carries the full narrative + trend surface from ai_expertise_impact: ai_flag, both trend fields, both expertise narratives, and the coverage block.

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)
  • 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/1/ai-impact
GET /api/clusters/187/ai-impact

DSL twin: GET AI_IMPACT FOR CLUSTER "187".

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 transition pathways for occupation or cluster.

Examples:

GET /api/occupations/15-1252.00/pathways
GET /api/occupations/cluster:5/pathways

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 ~55,000 assessed pairs:

{
  "success": true,
  "data": {
    "group_by": "ARCHETYPE",
    "by_archetype": {
      "early_career": { "platinum":  8996, "gold":  6852, "ranked": 15848 },
      "growth":       { "platinum": 18541, "gold": 14634, "ranked": 33175 },
      "stability":    { "platinum": 10428, "gold": 10376, "ranked": 20804 }
    },
    "total_rows": 54770
  }
}

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"
    }
  ]
}

GET /api/clusters/{id}

Get cluster details with expanded info including AI flag and education share.

Response Fields: cluster_id, cluster_name, cluster_occs, occupation_count, job_level, skill_adj_cluster_1/2/3, premium_skill_1/2/3, ba_plus_share, ai_flag

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. Each baseline exposes up to 3 underlying skill-adjacent entries; filters and metric-thresholds reduce that set further.

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 premium_skill_1/2/3
experience_level string "10" Snapshot key. Two key spaces exist: wage_data uses years ("0", "5", "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 Developers" },
  "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",
      "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_1/2/3 instead of skill_adj_cluster_1/2/3) 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 ONET (0.14.8+), so limit counts distinct occupations. Company-scoped calls and group=onet are unaffected. - 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",
      "onet_frequency": 2033,
      "match_score": 1.0,
      "confidence": "high",
      "match_source": "name_corpus",
      "recommended": true
    }
  ],
  "disambiguation": {
    "status": "resolved",
    "signals_used": ["historical_pattern"]
  }
}

Response Fields: | Field | Description | |-------|-------------| | onet_frequency | How many companies have this occupation (popularity) | | 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. | | match_source | Which matcher produced the row: name_corpus (fast typeahead, no-company, 0.15.0+) or database (legacy corpus / fusion path). | | recommended | true if this is the system's best recommendation | | 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
ba_plus_share float Share of practitioners with a bachelor's degree or higher (where present)
ai_flag string | null AI-impact label — see Methodology
entry_barrier_trend string | null Rising | Falling
expertise_premium_trend string | null Rising | Falling

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.

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