All platforms

Dover Jobs API.

Tap a single public JSON feed per Dover-powered careers board to pull every open role, with full HTML descriptions, office locations, and remote flags all arriving inline, no authentication or per-job requests.

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

Data fields
  • Full HTML Job Descriptions
  • Structured Job Locations
  • Remote Work Disposition
  • Office Names & Locations
  • Requisition & Internal IDs
  • First-Published & Updated Dates
Use cases
  1. 01Startup Job Aggregation
  2. 02Remote Role Discovery
  3. 03Recruiting Market Intelligence
  4. 04Careers Page Monitoring
DIY GUIDE

How to scrape Dover.

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

RESTbeginnerNo published limits; ~100ms between requests recommended, back off on 403/429No auth

Extract and lowercase the tenant slug

Dover boards live at app.dover.com/apply/{slug}. Pull the slug from the URL and lowercase it: the feed API is case-sensitive and only resolves the lowercase form.

Step 1: Extract and lowercase the tenant slug
import re

# Dover boards live at https://app.dover.com/apply/{slug}
APPLY_RE = re.compile(r"app\.dover\.com/apply/([^/?#]+)", re.IGNORECASE)

def tenant_slug(url: str) -> str:
    match = APPLY_RE.search(url)
    if not match:
        raise ValueError(f"Not a Dover apply URL: {url}")
    # The feed API is case-sensitive and only resolves the lowercase slug:
    # "SOUKMEDIA" 404s, "soukmedia" 200s.
    return match.group(1).lower()

slug = tenant_slug("https://app.dover.com/apply/SOUKMEDIA")
print(slug)  # "soukmedia"

Fetch the public jobs feed

Call the per-tenant feed endpoint once. It is public and unauthenticated, has no pagination, and returns every open role for the company under a top-level jobs array.

Step 2: Fetch the public jobs feed
import requests

def fetch_dover_jobs(slug: str) -> list[dict]:
    # One public, unauthenticated call returns every open role for the tenant.
    url = f"https://app.dover.com/feed/v1/boards/{slug}/jobs"
    response = requests.get(url, timeout=30)

    if response.status_code == 404:
        raise LookupError(f"Company '{slug}' not found on Dover")
    response.raise_for_status()

    # Response shape: {"jobs": [{"id", "title", "location": {"name"}, ...}]}
    return response.json().get("jobs", [])

jobs = fetch_dover_jobs("strattmont")
print(f"Found {len(jobs)} jobs")

Parse listings and build canonical URLs

Read the core fields from each job and construct the job URL yourself from (slug, id). Dover occasionally rewrites absolute_url to a different canonical slug, so building the URL keeps URL-to-company mapping self-consistent.

Step 3: Parse listings and build canonical URLs
def parse_listing(slug: str, job: dict) -> dict:
    job_id = job["id"]  # UUID string
    location = (job.get("location") or {}).get("name")

    return {
        "external_id": job_id,
        "title": (job.get("title") or "").strip(),
        "location": location,
        "remote": job.get("remote"),  # "only", "hybrid", or None
        # Build the URL from (slug, id) yourself. Dover sometimes rewrites
        # absolute_url to a different canonical slug (careerflow -> careerflowai),
        # which would break later URL -> company mapping.
        "url": f"https://app.dover.com/apply/{slug}/{job_id}",
    }

listings = [parse_listing("strattmont", job) for job in jobs]

Read the inline description and metadata

Dover ships the full description inline in the content field as HTML-entity-encoded markup, so no second request is needed per job. Decode it and pull the remaining metadata straight from the same payload.

Step 4: Read the inline description and metadata
import html

def parse_details(job: dict) -> dict:
    # The full description is already inline, so no per-job request is needed.
    description = html.unescape(job.get("content") or "")

    offices = [o["name"] for o in (job.get("offices") or []) if o.get("name")]

    return {
        "title": (job.get("title") or "").strip(),
        "company_name": job.get("company_name"),
        "description_html": description,
        "offices": offices,
        "requisition_id": job.get("requisition_id"),
        "internal_job_id": job.get("internal_job_id"),
        "first_published": job.get("first_published"),
        "updated_at": job.get("updated_at"),
    }

for job in jobs[:5]:
    d = parse_details(job)
    print(d["title"], "-", d["first_published"])
Common issues
highMixed-case tenant slug returns HTTP 404

Dover's /feed/v1/boards endpoint is case-sensitive and only resolves the lowercase slug. Lowercase the slug before requesting: 'SOUKMEDIA' 404s while 'soukmedia' returns 200.

mediumJob URLs break when trusting absolute_url

Dover sometimes rewrites absolute_url to a different canonical tenant slug (e.g. careerflow to careerflowai). Build the URL yourself from (slug, id) as https://app.dover.com/apply/{slug}/{id} to keep URL-to-company mapping consistent.

lowDescriptions contain raw HTML entities

The content field is HTML-entity-encoded (&amp;, &lt;, &#39;, &mdash;, etc.). Decode it with Python's html.unescape() before storing or displaying.

mediumBlocked or throttled with HTTP 403 / 429

Aggressive request rates can trigger a 403 block or a 429 rate limit. Keep a short delay (~100ms) between tenants and apply exponential backoff when either status is returned.

lowExpecting populated offices, department, or data_compliance

These arrays are frequently empty across tenants and remote may be null. Treat every one as optional and guard for missing or empty values rather than indexing directly.

Best practices
  1. 1Lowercase the tenant slug before every feed request
  2. 2Issue one feed request per company and reuse it - full descriptions are already inline
  3. 3Build job URLs from (slug, id) instead of trusting absolute_url
  4. 4Decode the content field with html.unescape() before storage
  5. 5Keep a short delay (~100ms) between tenants and back off on 403/429
  6. 6Treat remote, offices, and department as optional - they are often empty or null
Or skip the complexity

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

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

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