All platforms

Ashby Jobs API.

Pull complete postings — full HTML descriptions, pay ranges, departments, and locations — from a single public REST call, with no auth and no pagination to manage.

Get API access
Ashby
Live
100K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using Ashby
NotionFigmaLinearVercelRamp
Developer tools

Try the API.

Test Jobs and Feed endpoints against https://connect.jobo.world with live request/response examples, then copy ready-to-use curl commands.

What's in every response.

Data fields, real-world applications, and the companies already running on Ashby.

Data fields
  • Full HTML Descriptions
  • Structured Pay Ranges
  • Department & Team
  • Remote Flag & Locations
  • Employment Type
  • ISO Publish Dates
Use cases
  1. 01Startup Job Tracking
  2. 02Tech Talent Sourcing
  3. 03Compensation Benchmarking
  4. 04Remote Role Aggregation
Trusted by
NotionFigmaLinearVercelRampLimble CMMS
DIY GUIDE

How to scrape Ashby.

Step-by-step guide to extracting jobs from Ashby-powered career pages—endpoints, authentication, and working code.

RESTbeginnerNo published limit; throttle to avoid HTTP 429No auth

Fetch all job listings

Call the posting-api endpoint to retrieve every active job with full descriptions in a single request. This REST endpoint returns complete job data with no pagination.

Step 1: Fetch all job listings
import requests

company_slug = "limble"
url = f"https://api.ashbyhq.com/posting-api/job-board/{company_slug}"
params = {"includeCompensation": "true"}

response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()

# All jobs returned in a single response - no pagination needed
jobs = [j for j in data["jobs"] if j.get("isListed", True)]
print(f"Found {len(jobs)} active jobs for {company_slug}")

Parse job details from the response

Read fields off each job object. One call returns full HTML descriptions, plain-text versions, compensation, and structured address data — no per-job page fetch required.

Step 2: Parse job details from the response
for job in jobs:
    # Extract address details if available
    address = job.get("address", {}).get("postalAddress", {})

    job_data = {
        "id": job["id"],  # UUID format
        "title": job["title"],
        "department": job.get("department"),
        "team": job.get("team"),
        "location": job.get("location"),
        "city": address.get("addressLocality"),
        "country": address.get("addressCountry"),
        "is_remote": job.get("isRemote", False),
        "employment_type": job.get("employmentType"),
        "job_url": job.get("jobUrl"),
        "apply_url": job.get("applyUrl"),
        "published_at": job.get("publishedAt"),
        "salary": job.get("compensation", {}).get("compensationTierSummary"),
        "description_html": job.get("descriptionHtml", "")[:200] + "...",
        "description_plain": job.get("descriptionPlain", "")[:200] + "...",
    }
    print(f"{job_data['title']} - {job_data['location']}")

Validate the company slug

Confirm a board slug is real before scraping using the GraphQL organization endpoint. It returns the clean company name and public website, or null for an unknown slug.

Step 3: Validate the company slug
import requests

def validate_company(slug: str) -> dict | None:
    url = "https://jobs.ashbyhq.com/api/non-user-graphql"
    params = {"op": "ApiOrganizationFromHostedJobsPageName"}
    payload = {
        "operationName": "ApiOrganizationFromHostedJobsPageName",
        "variables": {
            "organizationHostedJobsPageName": slug,
            "searchContext": "JobBoard",
        },
        "query": """query ApiOrganizationFromHostedJobsPageName(
            $organizationHostedJobsPageName: String!,
            $searchContext: String
        ) {
            organization(
                organizationHostedJobsPageName: $organizationHostedJobsPageName
                searchContext: $searchContext
            ) { name publicWebsite hostedJobsPageSlug allowJobPostIndexing }
        }"""
    }
    resp = requests.post(url, json=payload, params=params, timeout=10)
    return resp.json()["data"]["organization"]  # None if invalid

# Test validation
org = validate_company("limble")
if org:
    print(f"Company: {org['name']}, Website: {org['publicWebsite']}")
else:
    print("Invalid company slug")

Handle secondary locations

A job can list more than one work location. Read the secondaryLocations array alongside the primary location to capture every option a candidate can pick.

Step 4: Handle secondary locations
def parse_all_locations(job: dict) -> list:
    locations = []

    # Primary location
    if job.get("location"):
        locations.append({
            "type": "primary",
            "location": job["location"],
            "remote": job.get("isRemote", False)
        })

    # Secondary locations (each entry only carries a location string)
    for loc in job.get("secondaryLocations", []):
        locations.append({
            "type": "secondary",
            "location": loc.get("location"),
        })

    return locations

# Process jobs with all location options
for job in jobs:
    all_locations = parse_all_locations(job)
    print(f"{job['title']}: {len(all_locations)} location(s)")

Scrape multiple companies politely

When iterating over many boards, add a short delay between requests and back off on HTTP 429 so you stay under Ashby's throttling.

Step 5: Scrape multiple companies politely
import time
import requests

def fetch_jobs_batch(slugs: list, delay: float = 0.6) -> dict:
    results = {}
    for slug in slugs:
        url = f"https://api.ashbyhq.com/posting-api/job-board/{slug}"
        try:
            resp = requests.get(url, params={"includeCompensation": "true"}, timeout=10)
            resp.raise_for_status()
            data = resp.json()
            active_jobs = [j for j in data.get("jobs", []) if j.get("isListed", True)]
            results[slug] = active_jobs
            print(f"{slug}: {len(active_jobs)} jobs")
        except requests.RequestException as e:
            print(f"Error fetching {slug}: {e}")
            results[slug] = []
        time.sleep(delay)  # add a delay between boards to avoid 429s
    return results

companies = ["limble", "ramp", "notion", "linear"]
all_jobs = fetch_jobs_batch(companies)
print(f"Total: {sum(len(j) for j in all_jobs.values())} jobs")

Build a complete scraper with error handling

Combine the steps into a robust scraper that validates the board, handles request failures, and returns only listed jobs.

Step 6: Build a complete scraper with error handling
import requests
import time

def scrape_ashby_company(slug: str) -> list[dict]:
    # Validate company first
    validate_url = "https://jobs.ashbyhq.com/api/non-user-graphql"
    validate_resp = requests.post(
        validate_url,
        json={
            "operationName": "ApiOrganizationFromHostedJobsPageName",
            "variables": {"organizationHostedJobsPageName": slug, "searchContext": "JobBoard"},
            "query": "query ApiOrganizationFromHostedJobsPageName($organizationHostedJobsPageName: String!, $searchContext: String) { organization(organizationHostedJobsPageName: $organizationHostedJobsPageName, searchContext: $searchContext) { name } }"
        },
        params={"op": "ApiOrganizationFromHostedJobsPageName"},
        timeout=10
    )

    if not validate_resp.json()["data"]["organization"]:
        print(f"Invalid company: {slug}")
        return []

    # Fetch jobs
    url = f"https://api.ashbyhq.com/posting-api/job-board/{slug}"
    resp = requests.get(url, params={"includeCompensation": "true"}, timeout=10)
    resp.raise_for_status()

    return [j for j in resp.json().get("jobs", []) if j.get("isListed", True)]

jobs = scrape_ashby_company("limble")
print(f"Scraped {len(jobs)} jobs")
Common issues
mediumCompany slug not found (returns empty jobs array)

The REST API returns {"jobs": []} for an unknown board. Slugs are case-sensitive and some contain spaces (e.g. 'Flock Safety'), so URL-encode them before requesting. Verify with the GraphQL organization endpoint, and check whether the company serves its board from a custom domain instead of jobs.ashbyhq.com.

lowMissing compensation data

Not every company exposes salary. Always send includeCompensation=true, then read both compensationTierSummary and scrapeableCompensationSalarySummary, treating null as 'not disclosed'.

mediumRate limiting / 429 errors

Ashby throttles aggressive scraping and returns HTTP 429 (and 403 when blocked); there is no published request budget. Add a short delay between requests and apply exponential backoff on 429.

lowisListed field filtering needed

Jobs with isListed=false are unlisted or draft postings. Always filter them out: [j for j in jobs if j.get('isListed', True)] so you never capture non-public roles.

highGraphQL query returns null organization

The GraphQL endpoint returns {"data": {"organization": null}} for an invalid slug. Make sure operationName matches the 'op' query parameter exactly, or fall back to the simpler REST posting-api.

mediumNo sitemap available for discovery

jobs.ashbyhq.com/sitemap.xml serves the app HTML, not an XML sitemap, so sitemap crawling fails. Discover boards from a maintained company list, Google dorks (site:jobs.ashbyhq.com), the Wayback Machine, or links on known career pages.

highAPI schema changes without versioning

The response carries apiVersion "1" but is not versioned in the URL, so fields can shift silently. Read every field defensively with .get() and monitor for changes.

Best practices
  1. 1Use the REST posting-api endpoint — it returns full descriptions, pay, and locations in one call with no pagination
  2. 2URL-encode board slugs: they are case-sensitive and some contain spaces (e.g. 'Flock Safety')
  3. 3Send includeCompensation=true and filter to isListed=true to skip draft and unlisted postings
  4. 4Add a short delay between requests and back off on HTTP 429 or 403
  5. 5Validate slugs with the GraphQL organization endpoint before bulk runs
  6. 6Cache results and refresh daily — boards rarely change intraday
Or skip the complexity

One endpoint. All Ashby jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=ashby" \
  -H "X-Api-Key: YOUR_KEY"
Ready to integrate

Access Ashby
job data today.

One API call. Structured data. No scraping infrastructure to build or maintain — start with the free tier and scale as you grow.

99.9%API uptime
<200msAvg response
50M+Jobs processed