Manatal Jobs API.
Pull structured job listings straight from Manatal's careers-page.com boards via a clean, paginated JSON API — full HTML descriptions, salary ranges, and locations, no authentication required.
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 Manatal.
- Full HTML Job Descriptions
- Structured Salary Ranges
- City, State & Country Locations
- Posted & Closing Dates
- Employment Type
- Direct Apply URLs
- 01Recruitment Agency Monitoring
- 02Job Board Aggregation
- 03Salary Benchmarking
- 04New Role Alerts
How to scrape Manatal.
Step-by-step guide to extracting jobs from Manatal-powered career pages—endpoints, authentication, and working code.
import requests
company_slug = "instaswim-llc"
url = f"https://www.careers-page.com/api/v1.0/c/{company_slug}/"
resp = requests.get(url, timeout=10)
resp.raise_for_status() # 404 here means the company slug is wrong
company = resp.json()
print(company.get("name"), company.get("website"))import requests
def fetch_manatal_jobs(company_slug: str) -> list[dict]:
jobs = []
page = 1
while True:
url = f"https://www.careers-page.com/api/v1.0/c/{company_slug}/jobs/"
params = {
"page": page,
"page_size": 20, # server hard-caps page_size at 20
"ordering": "-is_pinned_in_career_page,-last_published_at",
}
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status()
data = resp.json()
jobs.extend(data.get("results", []))
if not data.get("next"): # paginate until 'next' is null
break
page += 1
return jobs
all_jobs = fetch_manatal_jobs("instaswim-llc")
print(f"Found {len(all_jobs)} jobs")def parse_job(company_slug: str, job: dict) -> dict:
job_hash = job["hash"]
return {
"external_id": job_hash,
"title": (job.get("position_name") or "").strip(),
"description_html": job.get("description") or "",
"city": job.get("city"),
"state": job.get("state"),
"country": job.get("country"),
"location": job.get("location_display"),
"salary_min": job.get("salary_min"),
"salary_max": job.get("salary_max"),
"currency": job.get("currency_code"), # ISO code in the API
"salary_visible": job.get("is_salary_visible"),
"listing_url": f"https://www.careers-page.com/{company_slug}/job/{job_hash}",
"apply_url": f"https://www.careers-page.com/{company_slug}/job/{job_hash}/apply",
}
jobs = [parse_job("instaswim-llc", j) for j in all_jobs if j.get("hash")]import json
import re
import requests
JSON_LD_RE = re.compile(
r'<script[^>]*type="application/ld\+json"[^>]*>(.*?)</script>',
re.IGNORECASE | re.DOTALL,
)
def fetch_detail(listing_url: str) -> dict:
html = requests.get(listing_url, timeout=10).text
match = JSON_LD_RE.search(html)
if not match:
return {}
try:
ld = json.loads(match.group(1))
except json.JSONDecodeError:
return {} # malformed JSON-LD is non-fatal
return {
"posted_at": ld.get("datePosted"),
"closes_at": ld.get("validThrough"),
"employment_type": ld.get("employmentType"),
}
print(fetch_detail(jobs[0]["listing_url"]))import time
import requests
def safe_get(url, **kwargs):
resp = requests.get(url, timeout=10, **kwargs)
if resp.status_code == 404:
raise LookupError("Company or job not found")
if resp.status_code in (401, 403):
raise PermissionError("Blocked or authentication required")
if resp.status_code == 429:
time.sleep(5)
return safe_get(url, **kwargs)
resp.raise_for_status()
time.sleep(0.25) # ~250ms between requests, max ~5 concurrent details
return respThe server hard-caps page_size at 20. Do not assume a bigger page; instead loop pages and stop when the response 'next' field is null.
The slug in the careers-page.com URL is the tenant id. Confirm it against the /api/v1.0/c/{slug}/ endpoint, and note both www.careers-page.com and careers-page.com are valid hosts for the same board.
salary_min/salary_max are null when is_salary_visible is false. Trust currency_code from the listings API (ISO); the HTML detail page renders currency as a human-readable name like 'US Dollar' that you must map back to an ISO code.
Not every tenant populates city/state/country or location_display. Fall back gracefully and treat location as optional rather than assuming every job has it.
The description field is unsanitized HTML. Strip or sanitize it before rendering or indexing to avoid broken markup and injection risks.
Manatal blocks aggressive scraping. Add a ~250ms delay between requests, cap concurrent detail fetches (about 5), and back off on 429 before retrying.
- 1Paginate by following the 'next' field; never assume page_size can exceed 20
- 2Keep the ordering=-is_pinned_in_career_page,-last_published_at param for stable paging
- 3Prefer currency_code from the listings API (ISO) over the detail page's human-readable currency name
- 4Only fetch the HTML detail page when you need datePosted, validThrough or employmentType
- 5Space requests ~250ms apart and cap detail concurrency to avoid 403/429 blocks
- 6Sanitize the raw HTML description before storing or displaying it
One endpoint. All Manatal jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=manatal" \
-H "X-Api-Key: YOUR_KEY" Access Manatal
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.