All platforms

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.

Get API access
Manatal
Live
<3haverage discovery time
1hrefresh interval
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 Manatal.

Data fields
  • Full HTML Job Descriptions
  • Structured Salary Ranges
  • City, State & Country Locations
  • Posted & Closing Dates
  • Employment Type
  • Direct Apply URLs
Use cases
  1. 01Recruitment Agency Monitoring
  2. 02Job Board Aggregation
  3. 03Salary Benchmarking
  4. 04New Role Alerts
DIY GUIDE

How to scrape Manatal.

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

HybridintermediateNo published limit; self-throttle to ~250ms between requests and ~5 concurrent detail fetchesNo auth

Resolve the company board

Every tenant lives on careers-page.com under a company slug — validate it and pull the display name, logo, and full role list from one company endpoint.

Step 1: Resolve the company board
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"))

Fetch job listings with pagination

The listings API is a standard DRF-paginated endpoint. page_size is hard-capped at 20 by the server, so ignore larger values and follow the 'next' field until it is null. The ordering param keeps pinned roles first for stable paging.

Step 2: Fetch job listings with pagination
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")

Map fields and build canonical URLs

Each result carries the full HTML description, structured salary, and location parts. The listing and apply URLs are derived from the company slug plus the job 'hash'; skip any row without a hash.

Step 3: Map fields and build canonical URLs
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")]

Enrich from the detail page (dates + type)

The listings API already has title, description, location and salary. The HTML detail page only adds datePosted, validThrough and employmentType via a JSON-LD JobPosting block, so only fetch it when you need those extra fields.

Step 4: Enrich from the detail page (dates + type)
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"]))

Handle errors and throttle requests

Map status codes the way the endpoint uses them: 404 = missing company or job, 401/403 = blocked, 429 = rate limited. Space requests roughly 250ms apart and keep detail concurrency low to stay under the block threshold.

Step 5: Handle errors and throttle requests
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 resp
Common issues
mediumRequesting page_size above 20 still returns only 20 rows

The 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.

highCompany slug returns HTTP 404

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.

lowSalary is missing or the currency looks wrong

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.

lowSome postings have no location fields

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.

lowDescriptions arrive as raw HTML

The description field is unsanitized HTML. Strip or sanitize it before rendering or indexing to avoid broken markup and injection risks.

mediumBursts of requests get 403 or 429 responses

Manatal blocks aggressive scraping. Add a ~250ms delay between requests, cap concurrent detail fetches (about 5), and back off on 429 before retrying.

Best practices
  1. 1Paginate by following the 'next' field; never assume page_size can exceed 20
  2. 2Keep the ordering=-is_pinned_in_career_page,-last_published_at param for stable paging
  3. 3Prefer currency_code from the listings API (ISO) over the detail page's human-readable currency name
  4. 4Only fetch the HTML detail page when you need datePosted, validThrough or employmentType
  5. 5Space requests ~250ms apart and cap detail concurrency to avoid 403/429 blocks
  6. 6Sanitize the raw HTML description before storing or displaying it
Or skip the complexity

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

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

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.

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