All platforms

AllHires Jobs API.

Extract structured vacancies from AllHires-hosted career boards — practice group, PQE level, salary, and UK closing dates included — through a single public JSON API with no account required.

Get API access
AllHires
Live
<3haverage discovery time
1hrefresh interval
Companies using AllHires
Lewis SilkinShoosmithsAkin Gump
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 AllHires.

Data fields
  • Practice Group & Department
  • PQE & Experience Level
  • Salary & Employment Type
  • Full HTML Descriptions
  • UK Closing Dates
  • Structured Locations
Use cases
  1. 01Legal Recruitment Aggregation
  2. 02Law Firm Vacancy Tracking
  3. 03Job Board Syndication
  4. 04Salary & PQE Benchmarking
Trusted by
Lewis SilkinShoosmithsAkin Gump
DIY GUIDE

How to scrape AllHires.

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

RESTadvancedUndocumented; self-pace ~2 concurrent, ~200ms apart (429 on overload)No auth

Bootstrap the session token

AllHires guards its candidate API with an anti-CSRF token that is served both as a hidden form field and as a cookie on the /app/ board page. Load that page first and capture the token so later JSON calls are accepted.

Step 1: Bootstrap the session token
import time
import requests
from bs4 import BeautifulSoup

TENANT = "lewissilkin"            # first label of {tenant}.allhires.com
ORIGIN = f"https://{TENANT}.allhires.com"

session = requests.Session()

# 1. Load the public board so AllHires issues the anti-CSRF token.
boot = session.get(f"{ORIGIN}/app/", headers={"Accept": "text/html"})
boot.raise_for_status()

# The token is served twice: a hidden <input> and a Set-Cookie.
field = BeautifulSoup(boot.text, "html.parser").find(
    "input", {"name": "__RequestVerificationToken"})
request_token = field["value"].strip()
# session.cookies already holds the matching __RequestVerificationToken cookie.

List the public positions

Call the candidate Positions endpoint with the token echoed as a header (the cookie rides along from the session jar). The full vacancy array returns in a single response, so there is no pagination to follow — just drop placeholder rows without a positive ApplyingForID or title.

Step 2: List the public positions
def api_get(url, referer):
    # Every API call double-submits the token: header + cookie (from the jar).
    return session.get(url, headers={
        "Accept": "application/json",
        "__RequestVerificationToken": request_token,
        "Referer": referer,
    })

# The complete public list comes back in one request — no pagination.
positions = api_get(
    f"{ORIGIN}/webapi/candidate/Positions?isIntranetReq=false",
    f"{ORIGIN}/app/",
).json()["Positions"]

jobs = []
for p in positions:
    job_id = p.get("ApplyingForID")
    title = (p.get("CandidateTitle") or "").strip()
    if not job_id or job_id <= 0 or not title:
        continue  # skip unpublished / placeholder rows
    jobs.append({
        "id": job_id,
        "title": title,
        "department": p.get("ExternalDepartment"),
        "practice_group": p.get("PracticeGroup"),
        "location": p.get("LocationText"),
        "pqe_required": p.get("PQERequired"),
        "url": f"{ORIGIN}/app/PositionDetails?id={job_id}",
    })

Fetch and merge job details

For each position, request the detail endpoint by its native ApplyingForID to pull salary, employment type, closing date, and the description sections. Concatenate only the non-empty sections into the final HTML body.

Step 3: Fetch and merge job details
for job in jobs:
    detail = api_get(
        f"{ORIGIN}/webapi/candidate/position/details/?id={job['id']}&isIntranetReq=false",
        job["url"],
    ).json()
    d = detail["Details"]
    job["salary"] = d.get("Salary")
    job["employment_type"] = d.get("TermType")
    job["closing_date"] = d.get("ClosingDate")   # UK date, e.g. 31/12/2026
    # Rebuild the description from non-empty sections only.
    job["description"] = "".join(
        f"<h2>{(s.get('SectionHeader') or '').strip()}</h2>{s['SectionText']}"
        for s in detail["Sections"]
        if (s.get("SectionText") or "").strip()
    )
    time.sleep(0.2)  # ~200ms between requests

Handle session and rate-limit responses

The API answers 401/403 when the token expires (re-run the bootstrap), 429 when it is overloaded (back off and retry), and 404 for an unknown tenant host. Pace requests to a couple of concurrent calls, roughly 200ms apart, to stay under the limit.

Step 4: Handle session and rate-limit responses
resp = api_get(f"{ORIGIN}/webapi/candidate/Positions?isIntranetReq=false",
               f"{ORIGIN}/app/")

if resp.status_code in (401, 403):
    # Token rotated or session expired — re-run the bootstrap step.
    raise RuntimeError("Re-bootstrap /app/ to refresh __RequestVerificationToken")
elif resp.status_code == 429:
    time.sleep(5)          # rate limited: back off, then retry
elif resp.status_code == 404:
    raise RuntimeError("Unknown tenant — check the {tenant}.allhires.com host")
Common issues
highThe candidate API returns HTTP 401/403 even though the board is public.

AllHires requires the __RequestVerificationToken sent as both a request header and a cookie. Bootstrap /app/ to obtain both, and re-fetch them if the session starts returning 401/403.

mediumGraduate portals and marketing hosts do not expose the Positions API.

Only scrape {tenant}.allhires.com boards. Skip www, info, and support subdomains, plus authenticated *.grad.allhires.com graduate application portals — none serve the public candidate API.

lowSome Positions rows are placeholders or unpublished and have no usable job.

Drop rows where ApplyingForID is missing or <= 0, or where CandidateTitle is blank, before building canonical /app/PositionDetails?id= URLs.

mediumClosing dates parse to the wrong day when read as a US date.

ClosingDate is a UK (Europe/London) date such as 31/12/2026. Parse it as dd/mm/yyyy at end-of-day rather than the mm/dd/yyyy default.

Best practices
  1. 1Bootstrap /app/ once per tenant and reuse the token and cookie across all API calls.
  2. 2Send __RequestVerificationToken as both a request header and a cookie (double-submit).
  3. 3Skip Positions rows where ApplyingForID <= 0 or CandidateTitle is blank.
  4. 4Concatenate only non-empty description sections and treat a fully empty body as a parse failure.
  5. 5Pace requests to ~2 concurrent with ~200ms spacing and back off on HTTP 429.
  6. 6Restrict scraping to {tenant}.allhires.com; exclude www, info, support, and *.grad hosts.
Or skip the complexity

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

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

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