All platforms

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.

Get API access
Deel
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 Deel.

Data fields
  • Full Job Descriptions
  • Structured Locations
  • Employment Type
  • Posted & Closing Dates
  • Hiring Organization & Logo
  • Stable UUID Job IDs
Use cases
  1. 01Remote Job Aggregation
  2. 02Talent Market Monitoring
  3. 03Job Board Syndication
  4. 04Hiring Trend Analysis
DIY GUIDE

How to scrape Deel.

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

HTMLintermediateNo published limit; boards return HTTP 403/429 under aggressive loadNo auth

Fetch the tenant board and read its ItemList JSON-LD

Deel hosts each company board at jobs.deel.com/{tenant}. The page is Next.js server-rendered HTML with no public JSON API, so extract the ItemList JSON-LD embedded in the markup. Invalid tenants return HTTP 200 with a 'Job Board Not Found' title, so check for that before parsing.

Step 1: Fetch the tenant board and read its ItemList JSON-LD
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")

Collect job-detail URLs and stable UUID IDs

Each entry in itemListElement carries a url pointing at a /{tenant}/job-details/{uuid}/overview page. The UUID is the durable external ID for the job; capture it so IDs stay stable even if Deel changes the page path layout.

Step 2: Collect job-detail URLs and stable UUID IDs
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 jobs

Fetch each job page and parse the JobPosting JSON-LD

Request the detail URL and read the JobPosting JSON-LD. It carries title, description, datePosted, validThrough, employmentType, hiringOrganization, and jobLocation (with addressLocality / addressRegion / addressCountry). A 404 means the job was removed, not an error.

Step 3: Fetch each job page and parse the JobPosting JSON-LD
def 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,
    }

Throttle politely and run the full crawl

Deel returns HTTP 403/429 under aggressive load, so keep concurrency low (roughly 3 detail requests at a time) and space requests by about 400ms. Combine the listing and detail passes into one crawl per tenant.

Step 4: Throttle politely and run the full crawl
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"])
Common issues
highInvalid tenants return HTTP 200 with a 'Job Board Not Found' page

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.

mediumNo public JSON or REST endpoint exists for listings or details

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.

mediumTenant slugs are sometimes suffixed with a timestamp

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.

mediumAggressive scraping triggers HTTP 403 or 429

Limit to about 3 concurrent detail requests and add ~400ms between calls. Back off with exponential retry when you see 403 or 429 responses.

lowDetail pages missing a title or description

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.

Best practices
  1. 1Parse the ItemList JSON-LD for canonical detail URLs instead of scraping anchor tags
  2. 2Use the job-details UUID as the external ID so it survives page-layout changes
  3. 3Detect the 'Job Board Not Found' title even on HTTP 200 to avoid ingesting empty boards
  4. 4Preserve the full first path segment, including any -<timestamp> suffix, as the tenant
  5. 5Throttle to ~3 concurrent detail requests spaced ~400ms apart to avoid 403/429
  6. 6Treat a 404 on a detail page as a job removal, not a scrape error
Or skip the complexity

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

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

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.

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