Wage Data API

JSON wage percentiles (p10 to p90) for U.S. employers, computed from certified H-1B Labor Condition Application (LCA) filings published by the U.S. Department of Labor, next to BLS Occupational Employment and Wage Statistics (OEWS) benchmarks. Every response carries its sources.

Quickstart

Base URL: https://data-api.premed-toolkit.workers.dev. Send your key as X-API-Key: <key> or Authorization: Bearer <key>. The free tier is 100 successful calls per month at 10 per minute. /v1/health, /v1/openapi.json and this page need no key.

Get a free key

Enter your email, click the confirmation link we send, and your key (100 calls per month) arrives by email within about 15 minutes. One key per address.

Email me my free Wage Data API key from Filed Wages (one email after you confirm). Double opt-in, unsubscribe in one click. No spam, never sold. Privacy

curl

export WAGE_API_KEY="dak_your_key_here"
curl -s -H "X-API-Key: $WAGE_API_KEY" "https://data-api.premed-toolkit.workers.dev/v1/wages/employer/amazon"
curl -s -H "X-API-Key: $WAGE_API_KEY" "https://data-api.premed-toolkit.workers.dev/v1/wages/occupation/15-1252?metro=san-jose"
curl -s -H "X-API-Key: $WAGE_API_KEY" "https://data-api.premed-toolkit.workers.dev/v1/wages/occupation?title=software%20engineer&state=CA"
curl -s -H "X-API-Key: $WAGE_API_KEY" "https://data-api.premed-toolkit.workers.dev/v1/usage"

Python

import json, os, urllib.parse, urllib.request

BASE = "https://data-api.premed-toolkit.workers.dev"
KEY = os.environ["WAGE_API_KEY"]  # never hard-code a key

def get(path, **params):
    url = BASE + path + ("?" + urllib.parse.urlencode(params) if params else "")
    req = urllib.request.Request(url, headers={"X-API-Key": KEY})
    with urllib.request.urlopen(req, timeout=20) as r:
        return json.load(r)

emp = get("/v1/wages/employer/amazon")
print(emp["data"]["wage_percentiles_usd"], emp["data"]["median_vs_oews_ratio"])
occ = get("/v1/wages/occupation", title="software engineer", state="CA")
print(occ["data"]["occupation"], occ["data"]["p50"], occ["data"]["area_level"])  # p50 can be None when BLS suppresses it

JavaScript (Node 18+, server side)

const BASE = "https://data-api.premed-toolkit.workers.dev";
const KEY = process.env.WAGE_API_KEY; // never ship a key in browser code

async function get(path, params = {}) {
  const qs = new URLSearchParams(params).toString();
  const res = await fetch(BASE + path + (qs ? "?" + qs : ""), { headers: { "X-API-Key": KEY } });
  const body = await res.json();
  if (!res.ok) throw new Error(body.error?.code + ": " + body.error?.message);
  return body;
}

const emp = await get("/v1/wages/employer/amazon");
console.log(emp.data.wage_percentiles_usd, emp.data.median_vs_oews_ratio);

Google Sheets (Apps Script)

Extensions, Apps Script, paste, then Project Settings, Script properties, add WAGE_API_KEY. Use =WAGE_MEDIAN("software engineer", "CA") in a cell. Results are cached for six hours so a sheet does not burn quota.

const BASE = "https://data-api.premed-toolkit.workers.dev";

/** Median annual wage (BLS OEWS) for a job title in a two-letter state. */
function WAGE_MEDIAN(title, state) {
  const key = PropertiesService.getScriptProperties().getProperty("WAGE_API_KEY");
  if (!key) throw new Error("Set the WAGE_API_KEY script property");
  const url = BASE + "/v1/wages/occupation?title=" + encodeURIComponent(title) +
    (state ? "&state=" + encodeURIComponent(state) : "");
  const cache = CacheService.getScriptCache(), ck = Utilities.base64EncodeWebSafe(url).slice(0, 200);
  const hit = cache.get(ck);
  if (hit) return Number(hit);
  const res = UrlFetchApp.fetch(url, { headers: { "X-API-Key": key }, muteHttpExceptions: true });
  const body = JSON.parse(res.getContentText());
  if (res.getResponseCode() !== 200) throw new Error(body.error ? body.error.code : "HTTP " + res.getResponseCode());
  if (body.data.p50 === null) throw new Error("No benchmark (suppressed or top coded)");
  cache.put(ck, String(body.data.p50), 21600);
  return body.data.p50;
}

Endpoint reference

Generated from the live OpenAPI document at /v1/openapi.json. Success bodies are {data, attribution, meta}. Only 2xx GET responses count toward quota; 4xx and 5xx are refunded and HEAD is never metered. Headers: x-ratelimit-*, x-quota-*, retry-after.

GET /v1/wages/employer/{slug}

H-1B/LCA wage percentiles for one employer

ParameterInTypeNotes
slugpath, requiredstring Example: amazon

Responses:

GET /v1/wages/occupation/{soc}

Occupation wages by metro (LCA vs OEWS)

ParameterInTypeNotes
socpath, requiredstring Example: 15-1252
metroquerystringMetro slug, MSA code or name fragment. Omit to list metros. Example: san-jose
limitqueryintegerMetros listed when metro is omitted.

Responses:

GET /v1/wages/search

Search employers and occupations

ParameterInTypeNotes
qquery, requiredstring Example: google
typequerystring (employer | occupation)
limitqueryintegerCapped per plan (10 to 50).

Responses:

GET /v1/wages/occupation

Free-text job title to BLS OEWS percentiles (metro, then state, then national) The title is matched to an SOC code against every OEWS SOC title and O*NET alternate titles, with a score. Below 0.6 the API returns 404 no_match with suggestions instead of guessing. A city is used only when it names a principal city of a metro in the requested state; otherwise the real BLS state estimate (or national estimate) is returned.

ParameterInTypeNotes
titlequery, requiredstring Example: software engineer
levelquerystringAccepted for client compatibility; not used.
statequerystringTwo-letter U.S. state code (50 states, DC, PR, GU, VI). Anything else is 400 bad_state. Example: TX
cityquerystringWhole city name; fewer than 3 characters is ignored. Example: Austin

Responses:

GET /v1/wages/employer

Employer wage percentiles by name, optional job title Extension plan: needs X-License-Key with a valid RangeCheck Pro license (403 pro_required otherwise; 503 license_check_unavailable when Polar cannot be reached).

ParameterInTypeNotes
namequery, requiredstring Example: Amazon
titlequerystringWith title, p25/p50/p75/filings are for that employer's matching occupation and p10/p90 are null. Example: software
X-License-KeyheaderstringExtension plan only.

Responses:

GET /v1/usage

Current month usage and limits for your key (not metered)

Responses:

GET /v1/health

Liveness and dataset versions (no key needed)

Responses:

Coverage, read live

Right now the API serves 500 employers and 300 occupation by metro pairs with LCA percentiles, plus BLS OEWS May 2025 percentiles for 830 occupations nationally, by state and by metro area (139,505 metro estimates). Employer data last loaded 2026-10-21.

These numbers are read from the same store that answers your calls (see /v1/health). An employer outside the set returns 404 not_found, which is free. The API loads every employer and every occupation by metro pair with at least 5 certified filings (every group that can carry percentiles), with the full occupation list of each employer; groups under 5 filings never get percentiles.

Read this before you rely on the numbers

LCA wages are the bottom of the offered range. Each filing's wage is the low end of the range the employer certified (WAGE_RATE_OF_PAY_FROM), so employer and LCA medians run low against what people are actually paid and against OEWS. Percentiles are unweighted per filing, and describe wages certified on visa filings, not all workers. Use the OEWS fields for the general labor market. median_vs_oews_ratio and ratio_to_oews compare the two. Percentiles can be null when BLS suppresses an estimate or top codes it.

Small groups move with one filing. A group with 5 to 9 filings still gets percentiles, but its p25, p50 and p75 are individual filings or sit between two adjacent filings, so one new application can move them, and p10 and p90 are interpolated between the two lowest or the two highest filings. Every response carries filings; use it to apply your own floor (for example, ignore groups under 10 or 20 filings). Informational only; not legal, immigration or financial advice.

Attribution requirements

Keep the attribution block (sources, release dates and notice) with any data you republish or display, or reproduce it as a visible credit. Derived from U.S. Department of Labor OFLC LCA disclosure data and BLS OEWS (U.S. government works). Not affiliated with or endorsed by DOL or BLS. Job-title matching uses the O*NET 30.0 Database by the U.S. Department of Labor, Employment and Training Administration (USDOL/ETA), used under the CC BY 4.0 license. Modified: titles were normalised and mapped to OEWS codes. USDOL/ETA has not approved, endorsed, or tested these modifications. O*NET® is a trademark of USDOL/ETA. Anything that uses title matching must carry that O*NET notice, which is also in every response's attribution.notice.

Pricing

PlanPriceQuotaRate limitMax search results
free$0100 successful calls / month10 / minute10Get a free key
basic$9 / month5,000 successful calls / month60 / minute25Subscribe
pro$29 / month50,000 successful calls / month300 / minute50Subscribe
ultra$99 / month500,000 successful calls / month600 / minute50Subscribe

Only successful (2xx) calls count. Over quota you get 429 quota_exceeded until the next UTC month. Checkout is handled by Polar. Your key arrives by email (the address you pay with) within about 15 minutes of checkout. Cancel any time in Polar's customer portal; when a subscription ends or is refunded, its key drops to the free plan.