Deel Jobs API.
Every Deel-hosted careers board embeds clean JSON-LD, so you can pull structured titles, locations, and posting dates without rendering a single page.
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 Deel.
- Full Job Descriptions
- Structured Locations
- Employment Type
- Posted & Closing Dates
- Hiring Organization & Logo
- Stable UUID Job IDs
- 01Remote Job Aggregation
- 02Talent Market Monitoring
- 03Job Board Syndication
- 04Hiring Trend Analysis
How to scrape Deel.
Step-by-step guide to extracting jobs from Deel-powered career pages—endpoints, authentication, and working code.
import json
import re
import requests
HOST = "https://jobs.deel.com"
HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; JoboBot/1.0)"}
def find_jsonld(html: str, type_name: str) -> dict | None:
"""Return the first JSON-LD object whose @type matches type_name."""
blocks = re.findall(
r'<script[^>]+type="application/ld\+json"[^>]*>(.*?)</script>',
html,
re.DOTALL,
)
for block in blocks:
try:
data = json.loads(block)
except json.JSONDecodeError:
continue
for node in data if isinstance(data, list) else [data]:
if isinstance(node, dict) and node.get("@type") == type_name:
return node
return None
def fetch_board(tenant: str) -> dict | None:
resp = requests.get(f"{HOST}/{tenant}", headers=HEADERS, timeout=30)
resp.raise_for_status()
if "<title>Job Board Not Found</title>" in resp.text:
raise ValueError(f"Deel board '{tenant}' does not exist")
return find_jsonld(resp.text, "ItemList")JOB_ID_RE = re.compile(
r"/job-details/"
r"([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})",
re.IGNORECASE,
)
def list_jobs(tenant: str) -> list[dict]:
item_list = fetch_board(tenant)
if not item_list:
return []
jobs, seen = [], set()
for item in item_list.get("itemListElement", []):
detail_url = item.get("url") if isinstance(item, dict) else None
if not detail_url:
continue
match = JOB_ID_RE.search(detail_url)
if not match:
continue
job_id = match.group(1).lower()
if job_id in seen:
continue
seen.add(job_id)
jobs.append({"external_id": job_id, "listing_url": detail_url})
return jobsdef fetch_job(detail_url: str) -> dict | None:
resp = requests.get(detail_url, headers=HEADERS, timeout=30)
if resp.status_code == 404:
return None # job removed
resp.raise_for_status()
posting = find_jsonld(resp.text, "JobPosting")
if not posting or not posting.get("title") or not posting.get("description"):
return None
org = posting.get("hiringOrganization") or {}
raw_locations = posting.get("jobLocation") or []
if isinstance(raw_locations, dict):
raw_locations = [raw_locations]
locations = []
for loc in raw_locations:
addr = (loc or {}).get("address") or {}
locations.append({
"city": addr.get("addressLocality"),
"region": addr.get("addressRegion"),
"country": addr.get("addressCountry"),
"postal_code": addr.get("postalCode"),
})
return {
"title": posting.get("title"),
"description": posting.get("description"),
"employment_type": posting.get("employmentType"),
"posted_at": posting.get("datePosted"),
"closes_at": posting.get("validThrough"),
"company_name": org.get("name"),
"company_logo": org.get("logo"),
"url": posting.get("url", detail_url),
"locations": locations,
}import time
def crawl(tenant: str) -> list[dict]:
results = []
for job in list_jobs(tenant):
detail = fetch_job(job["listing_url"])
if detail:
results.append({**job, **detail})
time.sleep(0.4) # ~400ms between requests, matching polite defaults
return results
if __name__ == "__main__":
for job in crawl("klarna"):
print(job["external_id"], job["title"])Do not trust the status code alone. Check the response body for '<title>Job Board Not Found</title>' and treat it as a missing board before attempting to parse JSON-LD.
Probes for /{tenant}.json, /api/{tenant}/jobs, and /api/jobs?tenant=... return HTML or 404. Parse the ItemList and JobPosting JSON-LD embedded in the server-rendered HTML instead of hunting for an API.
Some boards use slugs like dott-1769520315673 rather than a clean company name. Always use the full first path segment (including any -<timestamp> suffix) as the tenant identifier.
Limit to about 3 concurrent detail requests and add ~400ms between calls. Back off with exponential retry when you see 403 or 429 responses.
Not every JobPosting is complete. Skip records where title or description is empty, and treat a 404 on a detail page as a removal signal rather than a hard failure.
- 1Parse the ItemList JSON-LD for canonical detail URLs instead of scraping anchor tags
- 2Use the job-details UUID as the external ID so it survives page-layout changes
- 3Detect the 'Job Board Not Found' title even on HTTP 200 to avoid ingesting empty boards
- 4Preserve the full first path segment, including any -<timestamp> suffix, as the tenant
- 5Throttle to ~3 concurrent detail requests spaced ~400ms apart to avoid 403/429
- 6Treat a 404 on a detail page as a job removal, not a scrape error
One endpoint. All Deel jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=deel" \
-H "X-Api-Key: YOUR_KEY" Access Deel
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.