Apple Jobs Jobs API.
Extract structured roles from Apple's first-party careers platform — a CSRF-guarded JSON search API backed by server-rendered detail pages, unified through Jobo's single jobs endpoint.
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 Apple Jobs.
- Full Job Descriptions
- Team Name & Code
- Multi-Location Roles
- Employment & Job Type
- Remote (Home Office) Flag
- Posting Dates
- 01Big Tech Hiring Signals
- 02UK Job Market Tracking
- 03Team & Org Structure Mapping
- 04Job Aggregator Feeds
How to scrape Apple Jobs.
Step-by-step guide to extracting jobs from Apple Jobs-powered career pages—endpoints, authentication, and working code.
import requests
session = requests.Session()
boot = session.get(
"https://jobs.apple.com/api/v1/CSRFToken",
headers={"Accept": "application/json"},
)
boot.raise_for_status()
# The token comes back on a response header; the cookies stay on the session.
csrf_token = boot.headers["X-Apple-CSRF-Token"]def fetch_page(session, csrf_token, page):
resp = session.post(
"https://jobs.apple.com/api/v1/search",
headers={
"Accept": "application/json",
"Content-Type": "application/json",
"X-Apple-CSRF-Token": csrf_token,
"Origin": "https://jobs.apple.com",
"Referer": "https://jobs.apple.com/en-gb/search?location=united-kingdom-GBR",
},
json={
"query": "",
"filters": {"locations": ["postLocation-GBR"]},
"page": page,
"locale": "en-gb",
"sort": "",
"format": {"longDate": "MMMM D, YYYY", "mediumDate": "MMM D, YYYY"},
},
)
resp.raise_for_status()
return resp.json()["res"]PAGE_SIZE = 20
def fetch_all(session, csrf_token):
seen, jobs, page = set(), [], 1
while True:
res = fetch_page(session, csrf_token, page)
rows = res.get("searchResults", [])
total = res.get("totalRecords", 0)
for row in rows:
position_id = row.get("positionId")
if not position_id or position_id in seen:
continue
seen.add(position_id)
jobs.append(row)
if not rows or page * PAGE_SIZE >= total:
break
page += 1
return jobsfrom urllib.parse import quote
def to_record(row):
position_id = row["positionId"]
slug = row.get("transformedPostingTitle") or "job"
team = row.get("team") or {}
return {
"id": position_id,
"title": row.get("postingTitle"),
"team": team.get("teamName"),
"team_code": team.get("teamCode"),
"locations": [loc.get("name") for loc in row.get("locations", [])],
"posted_at": row.get("postDateInGMT") or row.get("postingDate"),
"url": f"https://jobs.apple.com/en-gb/details/{quote(position_id)}/{quote(slug)}",
"apply_url": f"https://jobs.apple.com/app/en-gb/apply/{quote(position_id)}",
}import json, re
HYDRATION = re.compile(
r'window\.__staticRouterHydrationData\s*=\s*JSON\.parse\('
r'(?P<json>"(?:\\.|[^"\\])*")\);'
)
def fetch_details(session, detail_url):
html = session.get(detail_url, headers={"Accept": "text/html"}).text
match = HYDRATION.search(html)
if not match:
raise ValueError("Apple detail page omitted hydration data")
# The payload is a JSON string literal wrapping the real JSON document.
payload = json.loads(json.loads(match.group("json")))
job = payload["loaderData"]["jobDetails"]["jobsData"]
return {
"title": job.get("postingTitle"),
"employment_type": job.get("employmentType"),
"summary": job.get("jobSummary"),
"responsibilities": job.get("responsibilities"),
"minimum_qualifications": job.get("minimumQualifications"),
"preferred_qualifications": job.get("preferredQualifications"),
}Bootstrap /api/v1/CSRFToken before searching and forward both the X-Apple-CSRF-Token header and the returned session cookies (jobs, jssid, AWSALBAPP-*) on every call. A single requests.Session handles the cookies automatically.
The search API only returns a short jobSummary. Full descriptions, responsibilities, and qualifications live in the window.__staticRouterHydrationData blob on the server-rendered detail page — parse it from loaderData.jobDetails.jobsData.
Multi-location postings emit one search row per location with a different reqId suffix (e.g. 200671886-5682). Dedupe on the numeric positionId, which is identical across every duplicate row.
Detail hydration reports the requisition as PIPE-{positionId} or REQ-{positionId}, not the raw key. Treat positionId as the canonical identifier and ignore the reqId prefix when linking search rows to detail pages.
Apple rate-limits rapid requests. Pace calls roughly 100ms apart, keep detail concurrency low (around 6), and back off on 429 responses.
- 1Bootstrap /api/v1/CSRFToken first and reuse its token plus cookies via a persistent requests.Session.
- 2Key jobs on the numeric positionId — it is stable across the search, detail, and apply routes.
- 3Dedupe search rows on positionId to drop location-indexed duplicates.
- 4Paginate in pages of 20 and stop once page * 20 >= totalRecords.
- 5Read full descriptions from the detail page hydration blob; there is no separate detail JSON API.
- 6Throttle detail requests (~100ms apart, low concurrency) to avoid 429 responses.
One endpoint. All Apple Jobs jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=apple jobs" \
-H "X-Api-Key: YOUR_KEY" Access Apple Jobs
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.