Practical guide
How to Audit Your Job Postings for Salary Ranges (Pay Transparency Check)
Last reviewed: October 22, 2026
Build a complete posting inventory, collect pay evidence, map coverage with a reviewer, and verify corrections on the public page. Keep data-quality flags separate from the decision about whether a law applies.
Build a complete ATS posting inventory
List every employer entity, career board and agency-run board. Record eligible work locations, including remote eligibility and reporting offices. Assign an owner to confirm employee counts. A simple “Remote” label cannot supply this information.
Fetch public posting data before applying title or department filters. A partial export is not an all-postings audit. Preserve source URL, response and UTC observation time. For Greenhouse:
mkdir -p evidence
date -u '+%Y-%m-%dT%H:%M:%SZ' > evidence/observed-at.txt
curl --fail --show-error --silent --location --max-time 30 \
'https://boards-api.greenhouse.io/v1/boards/stripe/jobs?content=true&pay_transparency=true' \
> evidence/greenhouse.json
jq -e '.jobs | type == "array"' evidence/greenhouse.json
sha256sum evidence/greenhouse.json > evidence/greenhouse.sha256
Replace stripe with your board token. The ATS export guide gives endpoints and paging for all five sources. Greenhouse content includes descriptions; consult the API documentation for structured pay rather than assuming every board supplies it. Greenhouse: Job Board API.
Extract salary ranges without guessing missing values
Prefer structured compensation. Ashby exposes it with includeCompensation=true; Lever documents optional salaryRange data. Keep minimum, maximum, currency, pay period and source field separately. Preserve multiple location tiers. Sources: Ashby: public Job Postings API and Lever: official Postings API documentation.
For prose, use a candidate finder that sends matches for review. Save this as audit_ranges.py. It looks for two money amounts separated by a hyphen or “to” in saved Greenhouse descriptions. Typographic separators are normalized in a working copy:
# Save as audit_ranges.py. Input: saved Greenhouse jobs JSON.
import csv, html, json, re, sys
from html.parser import HTMLParser
class Plain(HTMLParser):
def __init__(self):
super().__init__()
self.parts = []
def handle_data(self, text):
self.parts.append(text)
money = r"(?:USD|CAD|EUR|GBP|\$|€|£)\s*"
amount = r"\d[\d,]*(?:\.\d{1,2})?\s*[kK]?"
pattern = re.compile(
rf"{money}{amount}\s*(?:-|to)\s*(?:{money})?{amount}", re.I)
with open(sys.argv[1], encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data.get("jobs"), list):
raise SystemExit("Not a Greenhouse jobs response")
out = csv.writer(sys.stdout)
out.writerow(["id", "title", "location", "url", "status", "matches"])
for job in data["jobs"]:
parser = Plain()
parser.feed(html.unescape(job.get("content") or ""))
text = " ".join(parser.parts)
normalized = text.translate({0x2013: "-", 0x2014: "-"})
matches = pattern.findall(normalized)
status = "candidate_range" if matches else "no_text_range_found"
out.writerow([job["id"], job["title"], job.get("location", {}).get("name", ""),
job.get("absolute_url", ""), status, " | ".join(matches)])
python3 audit_ranges.py evidence/greenhouse.json > evidence/range-candidates.csv
This misses fixed pay, some localized formats, ranges without currency marks and structured-only pay. It can match expenses or bonus ranges. It does not infer currency from a dollar sign or determine legal coverage. Read the original wording for context. No text match does not prove the public posting omits pay.
Map remote locations and employer thresholds
Join each job to a worksheet preserving original labels, normalized country/state/province, eligible remote locations, worksite and reporting office. Keep ambiguity unresolved: “CA” may mean California or Canada, and “London” needs a country.
Review potential rules in the 2026 laws table. New York and Illinois can cover certain out-of-state roles through reporting links. Confirm coverage from employer records and official guidance rather than API metadata.
For a multi-location job, retain pay tiers with their locations. A Toronto range does not establish New York pay. Do not convert currencies or annualize hourly pay without supported assumptions and a reason to compare them.
Flag missing ranges and review the original posting
| Flag | Possible explanation | Action |
|---|---|---|
| No candidate range | Pay absent, structured-only, or missed by extraction | Read native fields and the public page. |
| Open-ended pay | An endpoint may be missing | Check the rule and approved compensation plan. |
| Multiple ranges | Locations, bonuses or currencies may be mixed | Map tiers to locations and pay types. |
| Range found | Numbers alone do not prove full disclosure | Review period, currency, benefits and good faith. |
Record visible wording, field path or page section, URL, observation time, reviewer, source/version, coverage evidence and resolution. Save a dated page or screenshot where context matters. Limit sharing of contact details in source descriptions.
Do not label every missing match a violation. Employee size, job type, posting date, exemptions and nexus require review. Likewise, a range-shaped string is not a compliance finding. Reversed values, mismatched periods and missing benefits can need correction.
Correct, re-fetch and record evidence
Have the posting owner update the ATS, then re-fetch and inspect the public page. Compare ID and URL so replacement postings are not confused with corrections. Record approval, correction time and new evidence hash. Set retention and access controls with HR and legal owners.
Repeat checks at an interval suited to your posting volume. Keep fetch failures alongside flagged records so an empty report is not mistaken for a clean inventory. Recheck changed pay and locations as well as new IDs.
Use the salary auditor as a review queue
The Pay Transparency Job Auditor reads the same five ATS sources. Description scanning is opt-in; descriptions are never exported. Start with all output rows to assess unknown data:
{
"boards": ["greenhouse:stripe", "ashby:ramp"],
"maxJobsPerBoard": 500,
"maxResults": 1000,
"scanDescriptionForPay": true,
"onlyFlagged": false,
"requestDelayMs": 500
}
These are caps, not a completeness guarantee. Raise limits for larger boards and inspect RUN_SUMMARY. Output includes hasPayRange, pay endpoints/currency/period, matched jurisdictions, status, issues and rule notes.
range_ok_signal means observed pay passed numeric checks. depends_on_employer_size indicates unresolved threshold coverage. Benefits, employee counts, reporting lines, exemptions and actual good faith remain outside the automated check.
For repeat audits, enable monitorMode and keep a stable monitorKey. The first successful scan is silent by default; emitBaselineOnFirstRun emits it. Keep the baseline and errors with your review process.
Frequently asked questions
Does a missing regex match prove a violation?
No. Extraction may miss pay, and coverage or exceptions may be unresolved. Inspect the public posting and official rule.
Should hourly pay be multiplied by 2,080 automatically?
No. Annual equivalence needs supported hours and pay assumptions. Preserve the original period when those facts are unknown.
Can one range cover all remote locations?
Review the actual offered compensation and applicable locations. Keep location-specific tiers separate.
Does the Actor verify employer size or benefits?
No. It extracts pay and flags observed signals. Employee counts, benefits, exemptions and coverage facts need human review.
Sources
Primary documentation and official authorities reviewed for this guide. Live responses are observations on the review date; source policies and laws can change.
- Greenhouse: Job Board API
- Lever: official Postings API documentation
- Ashby: public Job Postings API
- Python: standard library reference
- jq: official manual
- New York Department of Labor: pay transparency
- Illinois Department of Labor: posting FAQ
- Ontario: publicly advertised job posting requirements
- Ontario Regulation 476/24
Tool inputs, outputs and limitations: Pay Transparency Job Auditor documentation.