SeeMeHired Jobs API.

Pull every vacancy from a SeeMeHired employer board through an unauthenticated JSON API, keyed on the numeric employer ID so a renamed company slug never splits or merges the wrong employer.

Get API access

What's in every response.

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

Data fields

  • Full Job Descriptions
  • Numeric Employer IDs
  • Active & Closed Flags
  • Location Matching
  • Internal vs Public Jobs
  • Company Profile Data

Use cases

  1. 01UK Job Board Aggregation
  2. 02Care & Hospitality Hiring Feeds
  3. 03SMB Recruitment Research
  4. 04Careers Page Monitoring

Trusted by

  • Abicare
  • Altogether Care
  • Andras Hotels
DIY GUIDE

How to scrape SeeMeHired.

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

API type
Hybrid
Difficulty
intermediate
Rate limit
No published limit; ~100ms between requests, max 3 concurrent detail calls
Authentication
No auth

Recognise the two public route shapes

A job page is https://seemehired.com/jobs/{numericJobId} and carries no employer. An employer board is https://seemehired.com/opportunities/{slug}. Only the numeric employer ID is a stable key — production data showed slugs drifting, for example compass-children-s-residential-services becoming compass-care.

Step 1: Recognise the two public route shapes
from urllib.parse import urlparse

PUBLIC_HOST = "seemehired.com"

def parse_seemehired(url: str) -> dict:
    parsed = urlparse(url)
    if parsed.netloc.lower() != PUBLIC_HOST:
        raise ValueError("not a SeeMeHired URL")

    parts = [p for p in parsed.path.strip("/").split("/") if p]
    if len(parts) == 2 and parts[0] == "jobs" and parts[1].isdigit():
        return {"kind": "job", "job_id": parts[1]}
    if len(parts) == 2 and parts[0] == "opportunities":
        return {"kind": "board", "slug": parts[1]}
    raise ValueError("unrecognised SeeMeHired route")

print(parse_seemehired("https://seemehired.com/jobs/68594"))
# {'kind': 'job', 'job_id': '68594'}

Prove the employer from a tenantless job URL

A /jobs/{id} link names no company, so resolve it from the provider's own data. The public job record carries a numeric employerId, and the company endpoint turns that ID back into the current slug. Use the numeric ID as your key and the slug only for display.

Step 2: Prove the employer from a tenantless job URL
import requests

API = "https://api.seemehired.com/public"

def resolve_employer(job_id: str, session: requests.Session) -> dict | None:
    job = session.get(f"{API}/jobs/{job_id}", timeout=30)
    if job.status_code in (404, 410):
        return None  # the posting is gone
    job.raise_for_status()
    record = job.json()

    employer_id = record.get("employerId")
    if not isinstance(employer_id, int) or employer_id <= 0:
        return None

    company = session.get(f"{API}/companies/{employer_id}", timeout=30)
    company.raise_for_status()
    return {
        "employer_id": employer_id,
        "slug": company.json().get("slug"),
        "job_token": record.get("jobToken"),
    }

session = requests.Session()
print(resolve_employer("68594", session))

Re-check the slug before every listings run

This is the step that keeps the data honest. The paginated search endpoint takes a company slug, and for an unknown slug it silently returns an unrelated global result set instead of an error. Confirm the company endpoint still maps your numeric employer ID to that slug before you search.

Step 3: Re-check the slug before every listings run
def verified_slug(employer_id: int, session: requests.Session) -> str:
    resp = session.get(f"{API}/companies/{employer_id}", timeout=30)
    resp.raise_for_status()
    slug = resp.json().get("slug")
    if not slug:
        raise RuntimeError(f"employer {employer_id} no longer publishes a slug")
    return slug

def listings_url(slug: str, page: int) -> str:
    return (
        f"{API}/jobs/search/paginated"
        f"?companySlug={slug}&locationMatch=exact&limit=500"
        f"&page={page}&internalJobs=false"
    )

slug = verified_slug(304, session)   # Abicare
print(listings_url(slug, 1))

Page the search endpoint and reject foreign rows

Request 500 rows per page and drive pagination off the root count and total fields. Every row must repeat the employer ID you asked for; if any row names a different employer, fail the whole snapshot rather than emitting it, so a bad response can never expire a real board.

Step 4: Page the search endpoint and reject foreign rows
def fetch_board(employer_id: int, slug: str, session: requests.Session) -> list[dict]:
    collected, page = [], 1
    while True:
        resp = session.get(listings_url(slug, page), timeout=30)
        resp.raise_for_status()
        payload = resp.json()

        rows = payload.get("jobs") or payload.get("results") or []
        total = payload.get("total")

        for row in rows:
            if row.get("employerId") != employer_id:
                raise RuntimeError(
                    "search returned a foreign employer — this is the unknown-slug "
                    "fallback, not this board. Abort instead of expiring jobs."
                )
            collected.append(row)

        if not rows or (total is not None and len(collected) >= total):
            return collected
        page += 1

jobs = fetch_board(304, slug, session)
print(f"{len(jobs)} rows for employer 304")

Hydrate details and classify unavailability precisely

Details come from the same public job endpoint. Distinguish three outcomes: HTTP 404 or 410 is canonical removal; isActive=false, closed=true, or isJobInternal=true is structured unavailability; anything else that fails to parse is an error and must never be read as removal.

Step 5: Hydrate details and classify unavailability precisely
import time

def hydrate(job_id: str, employer_id: int, session: requests.Session) -> dict:
    resp = session.get(f"{API}/jobs/{job_id}", timeout=30)
    if resp.status_code in (404, 410):
        return {"id": job_id, "state": "removed"}
    resp.raise_for_status()
    record = resp.json()

    if record.get("employerId") != employer_id:
        raise RuntimeError("detail contradicted the listing employer")

    if not record.get("isActive") or record.get("closed") or record.get("isJobInternal"):
        return {"id": job_id, "state": "unavailable"}

    return {
        "id": job_id,
        "state": "active",
        "title": record.get("title"),
        "description_html": record.get("description"),
        "listing_url": f"https://seemehired.com/jobs/{job_id}",
        "apply_url": f"https://seemehired.com/jobs/{slug}/{job_id}?company={slug}",
    }

for row in jobs[:3]:
    print(hydrate(str(row["id"]), 304, session)["state"])
    time.sleep(0.1)
Common issues
criticalWhy does searching an unknown company slug return jobs anyway?
The paginated search endpoint falls back to an unrelated global result set instead of erroring on an unknown slug. Re-verify the slug through the company endpoint before every run and reject any row whose employerId differs from the board you asked for.
highWhy did an employer's slug stop matching my stored value?
SeeMeHired slugs are renamed in place — a live audit found three stale aliases in 145 rows. Store the numeric employerId as the stable key and re-read the current slug from the company endpoint each run, using it only to build display and apply URLs.
mediumWhy do most jobs come back with isActive false?
Boards keep historical postings addressable, so inactive rows greatly outnumber live ones — an audit found 123 of 144 proved jobs inactive. Treat isActive=false, closed=true, and isJobInternal=true as structured unavailability and publish only the active remainder.
lowWhy does a job page not link back to the employer's board?
Some employers set hideCompanyProfile, and around 15% of job pages omit the opportunities link entirely. The company endpoint keyed on the numeric employerId is the authoritative fallback; never reconstruct the board URL from a third-party company name.
Best practices
  1. 1Key the employer on the numeric employerId, never on the mutable slug
  2. 2Re-verify the slug through the company endpoint before every listings run
  3. 3Request limit=500 with internalJobs=false and page until count reaches total
  4. 4Abort the snapshot when any row names a different employer
  5. 5Separate HTTP 404/410 removal from isActive=false structured unavailability
  6. 6Keep the numeric employer ID and slug on every emitted row for audit trails
Or skip the complexity

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

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=seemehired" \
  -H "X-Api-Key: YOUR_KEY"
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.

Ready to integrate

Access SeeMeHired
job data today.

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

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