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/badgesand/api/occupations/badges(+/countsvariants 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 derivededucation_levelresponse field has been removed from/api/occupations/{id}/educationand/api/clusters/{id}/education. Clients should readba_plus_share(raw 0.0–1.0 share of workforce with a BA) and/orjob_level(Sub-Bachelor's Degree,Bachelor's Degree,Advanced Degree, optional+ Experience). - 0.12.3-beta (fix): TheRankedbadge 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}/companiesand/api/companies/{name}/clustersnow return each company'sprimary_industry; the former gains asort=industrymode. - 0.12.5-beta: New calculatedsort=wywmmode on/api/clusters/{id}/companies— ranks companies by a fixed badge×hiring priority, with an optionalarchetypeselector (early_careerdefault /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): Oncompany_occupation_summarythe occupation-levelpostings_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), andearly_career_access(0/1, response-only). The old?postings_qtile=filter and?sort=postings_qtilenow return HTTP 400 with a structured deprecation error — use?entry_posting_tier=/?total_posting_tier=and?sort=entry_postings/?sort=total_postings. Thesort=wywmhiring signal is now the textual tier and is archetype-dependent (early_career→entry_posting_tier;growth/stability→total_posting_tier). New opt-in?expand=timestampsaddscreated_at/updated_atto/api/companiesand/api/companies/{name}/occupations. The geographichiring_postings_qtile(1–3 onhiring_flag) is a different column and is unchanged. - 0.13.1-beta (feature — additive):/api/titleconvertgains an opt-in?expand=clusterthat attaches the occupation cluster (cluster_id+ canonicalcluster_name) to each match, resolving title → ONET → cluster in one call. The cluster name matches what/api/clusters/convertreturns. 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, soLIMITcounts distinct occupations (the flat per-row schema is preserved;company_nameis the top company). Company-scoped calls and?group=onetare unchanged. - 0.15.0-beta (behavioral — default ON; reversible): fast typeahead mode for the no-company/api/titleconvertpath — 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/titleconvertaccepts 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/titleconvertremains single-title; call it once per title, or use the DSL/MCP interface for bulk resolution. - 0.15.2-beta (behavioral —match_scorescale): on the/api/titleconvertdatabase / fusion path thematch_scoreis now 1.0-anchored — the best match anchors at1.0and 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 fixedmin_scorecutoff should re-check it against the new scale (see themin_scorenote under the endpoint). - 0.15.15-beta (feature — additive):POST /api/titleconvertrecords which result the user picked from a prior GET. Body piggybacks the GET envelope pluspicked(the chosendata[]row). Does not re-run matching. POST only (GET is cached at the proxy). DSL twins:SUBMIT TITLECONVERT TITLE "..." CHOICE "<onet_code>"andSUBMIT 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 ofGET /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 formeraoi_title_convert/aoi_company_converttools were removed — issueTITLECONVERT "..."/COMPANYCONVERT "..."as DSL throughaoi_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}/occupationsand/api/companies/{name}/badgesto 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¶
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
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:
Refresh Token¶
POST /auth/refresh
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_typeandcompany_event_noteare only present when non-empty. Theaoi_parents,aoi_subsidiaries, andaoi_siblingsfields are always returned as arrays ofcompany_uidvalues (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/{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. |
Rankedsentinel — same semantic as/api/companies?badge_X=Ranked: matches occupations at this company that hold any non-null tier on the targeted archetype. UseRankedwhen "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. Useentry_posting_tier/total_posting_tier(valueshigh|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:
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 isnullfor 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:
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:
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 clustercluster_id,cluster_name,count
Errors:
400 Bad Request—cluster_id must be an integer404 Not Found—Cluster not found
Examples:
DSL twin: GET AI_IMPACT FOR CLUSTER "187".
GET /api/occupations/{id}/education¶
Education requirements for occupation or cluster.
Examples:
GET /api/occupations/{id}/pathways¶
Career transition pathways for occupation or cluster.
Examples:
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). |
Rankedsentinel — passingRankedon anybadge_*field matches companies whose pair-level badge on that archetype is non-null (any ofPlatinum/Gold). Honored on this endpoint as of 0.12.3-beta — earlier releases returned 0 rows for this query.DEPRECATION (6-2-26):
postings_qtilewas retired —?postings_qtile=and?sort=postings_qtilereturn HTTP 400 with a deprecation error. Use the tier filters and?sort=entry_postings|total_postings(or DSLORDER 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§or=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:
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_qtilefilter and thepostings_qtilesort mode were retired. Sending?postings_qtile=or?sort=postings_qtilereturns HTTP 400 with a structured deprecation error. Useentry_posting_tier/total_posting_tier(filter) andsort=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_careerusesentry_posting_tier;growth/stabilityusetotal_posting_tier. Rows are ranked in this fixed priority order; rows whose chosen badge is null/NA or whose tier is missing get rank99and 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
archetypeparameter (defaultearly_career) selects which badge supplies the tier (and which posting-tier column is read). Each row carrieswywm_rank(1=best … 99=unranked); the response envelope echoeswywm_archetype. The exact bucket order may be tuned during the beta.Occupation-level tiers vs geographic quantile:
entry_posting_tier/total_posting_tierare textual tiers (high…none) oncompany_occupation_summary;hiring_postings_qtileis a 1–3 numeric quantile onhiring_flag(thehiring_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=0and?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 asnull.
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):
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:
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:
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_scoreon the default paths (0.14.5-beta+). Fusion ranking is default ON, and when it is active the legacymin_score=0.5hard 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 nomin_scoregate at all. Treatmin_scoreas effective only when you force the legacy pipeline (_fusion=falseand/or_fast=false). Combined with the 0.15.2-beta 1.0-anchoring (see Changes note), a fixed client-sidemin_scoremay 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):
Example (with context):
Example (with cluster):
Example (unique occupations — autocomplete):
Example (ask for clarification on an ambiguous title):
Example (force the legacy pipeline — company-scoped):
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
companyorindustryfor ambiguous titles. Usegroup=onetfor 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¶
- Cache tokens - Reuse access tokens until expiry
- Use pagination - Set
limitandoffsetfor large results - Use search tools -
titleconvertandcompanyconvertfor fuzzy matching - 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