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.
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.
- Structured Job Titles
- Department & Team Grouping
- Multi-Location Data
- Remote Work Flags
- Full Job Descriptions
- Posted Dates & Employment Type
- 01SMB Hiring Trends
- 02Job Board Aggregation
- 03Recruiting Market Research
- 04Careers Page Monitoring
How to scrape Freshteam.
Step-by-step guide to extracting jobs from Freshteam-powered career pages—endpoints, authentication, and working code.
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")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")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"])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')}")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 jobsFreshteam 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.
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.
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.
The JobPosting JSON-LD description is HTML-entity-encoded (<p> instead of <p>). Either html.unescape it or read the rendered description body from div.job-details-content > div on the detail page.
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.
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.
- 1Read data-portal-* attributes rather than visual CSS classes; they are far more stable across theme changes
- 2Pull full descriptions from the detail page DOM (div.job-details-content), not the entity-encoded JSON-LD string
- 3Trust the 'Preferable Location(s)' header block over JSON-LD jobLocation for real job geography
- 4Map department, job-type and location IDs to names from the page's select dropdowns
- 5Throttle to a few hundred ms between requests and cap concurrent detail fetches around three
- 6Validate the subdomain before scraping; all roles load on one page, so there is no pagination to follow
One endpoint. All Freshteam jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=freshteam" \
-H "X-Api-Key: YOUR_KEY" 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.