All platforms

Freshteam Jobs API.

Every Freshteam careers board embeds structured data-portal attributes and JobPosting JSON-LD, so you can pull clean job data without a login or a headless browser. Freshworks' SMB ATS renders all roles on a single page, so there is no pagination to crawl.

Get API access
Freshteam
Live
40K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using Freshteam
A1FED IncFreshworks
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 Freshteam.

Data fields
  • Structured Job Titles
  • Department & Team Grouping
  • Multi-Location Data
  • Remote Work Flags
  • Full Job Descriptions
  • Posted Dates & Employment Type
Use cases
  1. 01SMB Hiring Trends
  2. 02Job Board Aggregation
  3. 03Recruiting Market Research
  4. 04Careers Page Monitoring
Trusted by
A1FED IncFreshworks
DIY GUIDE

How to scrape Freshteam.

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

HTMLbeginnerNo published limit; keep to ~500 ms between requests and cap detail fetches at ~3 concurrent to avoid 403/429 blocksNo auth

Fetch the job listings page

Request the company's Freshteam board at /jobs. Every role is server-rendered onto this single page with embedded data attributes, so no pagination or JavaScript rendering is needed.

Step 1: Fetch the job listings page
import requests
from bs4 import BeautifulSoup

company_slug = "a1fed"
url = f"https://{company_slug}.freshteam.com/jobs"

response = requests.get(url, timeout=10)
soup = BeautifulSoup(response.content, "html.parser")

# Find all job listings with data attributes
job_elements = soup.select("a[data-portal-title]")
print(f"Found {len(job_elements)} jobs")

Extract job data from data attributes

Freshteam embeds structured data in data-portal-* attributes on each job anchor. Read these attributes for clean fields and resolve the relative href against the company subdomain.

Step 2: Extract job data from data attributes
jobs = []
for job_el in job_elements:
    job = {
        "title": job_el.get("data-portal-title", ""),
        "location": job_el.get("data-portal-location", ""),
        "job_type_id": job_el.get("data-portal-job-type", ""),
        "is_remote": job_el.get("data-portal-remote-location") == "true",
        "url": f"https://{company_slug}.freshteam.com{job_el.get('href', '')}",
    }
    # Extract display title from inner div
    title_div = job_el.select_one(".job-title")
    if title_div:
        job["display_title"] = title_div.get_text(strip=True)
    jobs.append(job)

print(f"Extracted {len(jobs)} jobs with structured data")

Build department and type mappings

The listing attributes carry IDs, not labels. Read the select dropdowns (department_id, work_type_id, city_id) to map each ID to its human-readable name; department names also appear in the role-section h5 headers.

Step 3: Build department and type mappings
def build_mappings(soup):
    mappings = {"departments": {}, "job_types": {}, "locations": {}}

    # Extract department mappings
    dept_select = soup.select_one("select#department_id")
    if dept_select:
        for option in dept_select.select("option[value]"):
            mappings["departments"][option["value"]] = option.get_text(strip=True)

    # Extract job type mappings
    type_select = soup.select_one("select#work_type_id")
    if type_select:
        for option in type_select.select("option[value]"):
            mappings["job_types"][option["value"]] = option.get_text(strip=True)

    # Extract location mappings
    loc_select = soup.select_one("select#city_id")
    if loc_select:
        for option in loc_select.select("option[value]"):
            mappings["locations"][option["value"]] = option.get_text(strip=True)

    return mappings

mappings = build_mappings(soup)
print("Departments:", mappings["departments"])

Fetch detailed job information via JSON-LD

Each detail page embeds a JobPosting JSON-LD block with title, dates and employment type. Note two quirks: its description is HTML-entity-encoded (unescape it), and jobLocation carries the company HQ rather than the role's geography.

Step 4: Fetch detailed job information via JSON-LD
import json

def get_job_details(job_url: str) -> dict:
    response = requests.get(job_url, timeout=10)
    soup = BeautifulSoup(response.content, "html.parser")

    # Find JSON-LD structured data
    script_tag = soup.select_one('script[type="application/ld+json"]')
    if script_tag:
        data = json.loads(script_tag.string)
        return {
            "title": data.get("title"),
            "description": data.get("description"),
            "date_posted": data.get("datePosted"),
            "employment_type": data.get("employmentType"),
            "is_remote": data.get("remote") == "true",
            "location": data.get("jobLocation", {}).get("address", {}),
        }
    return {}

# Get details for first job
if jobs:
    details = get_job_details(jobs[0]["url"])
    print(f"Job: {details.get('title')}")

Handle errors and throttle requests

Wrap fetches with error handling and short delays. Map HTTP status codes to outcomes: 404 means the board or job is gone, 401 means the private API path, and 403/429 mean you are being throttled.

Step 5: Handle errors and throttle requests
import time

def fetch_all_jobs(company_slug: str) -> list:
    url = f"https://{company_slug}.freshteam.com/jobs"
    jobs = []

    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        soup = BeautifulSoup(response.content, "html.parser")

        job_elements = soup.select("a[data-portal-title]")
        mappings = build_mappings(soup)

        for job_el in job_elements:
            job = {
                "title": job_el.get("data-portal-title", ""),
                "location": job_el.get("data-portal-location", ""),
                "is_remote": job_el.get("data-portal-remote-location") == "true",
                "url": f"https://{company_slug}.freshteam.com{job_el.get('href', '')}",
            }
            jobs.append(job)

    except requests.RequestException as e:
        print(f"Error fetching jobs: {e}")

    return jobs
Common issues
criticalThere is no public REST or GraphQL jobs API

Freshteam exposes no unauthenticated data endpoint: /api/jobs returns HTTP 401 JSON and /jobs.json returns HTML. Extract from the server-rendered /jobs page using the data-portal-* attributes and detail-page JSON-LD instead.

highWrong company subdomain returns HTTP 404

The board slug is the first label of the host (a1fed in a1fed.freshteam.com). Verify the subdomain before scraping; an unknown or inactive slug returns HTTP 404 rather than an empty page.

mediumDepartments, job types and locations appear only as IDs

The listing attributes store IDs. Parse the select#department_id, select#work_type_id and select#city_id dropdowns (or the role-section h5 headers) to resolve IDs to readable names.

mediumJSON-LD description comes back as encoded markup

The JobPosting JSON-LD description is HTML-entity-encoded (&lt;p&gt; instead of <p>). Either html.unescape it or read the rendered description body from div.job-details-content > div on the detail page.

mediumJSON-LD location shows the company HQ, not the role's location

jobLocation reflects the organization address (with region and locality reversed). For true geography, read the detail-page header block, which carries the canonical 'Preferable Location(s):' list for multi-location and remote roles.

lowBulk crawling triggers HTTP 403 or 429

There is no published rate limit, but aggressive fetching gets blocked. Space requests a few hundred milliseconds apart and cap concurrent detail fetches (~3) to stay under the throttle.

Best practices
  1. 1Read data-portal-* attributes rather than visual CSS classes; they are far more stable across theme changes
  2. 2Pull full descriptions from the detail page DOM (div.job-details-content), not the entity-encoded JSON-LD string
  3. 3Trust the 'Preferable Location(s)' header block over JSON-LD jobLocation for real job geography
  4. 4Map department, job-type and location IDs to names from the page's select dropdowns
  5. 5Throttle to a few hundred ms between requests and cap concurrent detail fetches around three
  6. 6Validate the subdomain before scraping; all roles load on one page, so there is no pagination to follow
Or skip the complexity

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

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

Access Freshteam
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