All platforms

CareerPlug Jobs API.

Aggregate hourly, franchise, and small-business openings from CareerPlug's server-rendered subdomain boards, each record carrying structured pay ranges, State-City-ZIP locations, and full job descriptions.

Get API access
CareerPlug
Live
25K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using CareerPlug
Sir SpeedyPuroCleanServPro
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 CareerPlug.

Data fields
  • Structured Pay Ranges
  • City, State & ZIP Locations
  • Full Job Descriptions
  • Employment Type
  • Job Post Dates
  • Direct Apply URLs
Use cases
  1. 01Hourly & Frontline Job Aggregation
  2. 02Franchise Hiring Feeds
  3. 03Local Job Board Ingestion
  4. 04SMB Recruitment Analytics
Trusted by
Sir SpeedyPuroCleanServPro
DIY GUIDE

How to scrape CareerPlug.

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

HTMLintermediateNo published limit; the AWS load balancer returns HTTP 403 when throttling. Cap concurrency (~3 detail requests) and space requests ~300ms apart.No auth

Fetch the job listings page

Request the /jobs page from the employer's CareerPlug subdomain. Each company lives on its own subdomain and the board is rendered fully server-side, so all listings arrive in the HTML.

Step 1: Fetch the job listings page
import requests
from bs4 import BeautifulSoup

company_slug = "bemobile"
url = f"https://{company_slug}.careerplug.com/jobs"

headers = {
    "User-Agent": "Mozilla/5.0 (compatible; JobScraper/1.0)",
    "Accept": "text/html",
}

response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
print(f"Fetched page: {response.url}")

Parse job listings from HTML

Extract titles, locations, and URLs from the job container. CareerPlug wraps each row in an <a href='/jobs/{id}'> and uses either #job_table or the legacy #job-list container depending on the theme.

Step 2: Parse job listings from HTML
# Find the job container (supports both modern and legacy layouts)
job_container = soup.select_one("#job_table, #job-list")

if job_container:
    job_links = job_container.select("div > a[href^='/jobs/']")
    jobs = []

    for link in job_links:
        title_elem = link.select_one(".job-title .name")
        location_elem = link.select_one(".job-location")

        jobs.append({
            "title": title_elem.get_text(strip=True) if title_elem else None,
            "location": location_elem.get_text(strip=True) if location_elem else None,
            "url": f"https://{company_slug}.careerplug.com{link['href']}",
            "job_id": link['href'].split('/')[-1],
        })

    print(f"Found {len(jobs)} jobs")
    for job in jobs[:3]:
        print(f"  - {job['title']} @ {job['location']}")

Fetch job details

Request each /jobs/{id} page for the full description. Some tenants only serve the complete description on the application page, so fall back to /jobs/{id}/apps/new when the detail page comes up empty.

Step 3: Fetch job details
def fetch_job_details(job_url: str) -> dict:
    """Fetch full job details from a CareerPlug job page."""
    response = requests.get(job_url, headers=headers, timeout=10)
    response.raise_for_status()
    soup = BeautifulSoup(response.text, "html.parser")

    # Extract job details
    title = soup.select_one("h1")
    description = soup.select_one(".trix-content, .job-description, [class*='description']")
    location_info = soup.select_one("h1 + p, [class*='location']")

    # Fall back to the application page when the description is missing
    if description is None:
        apply_url = job_url.rstrip("/") + "/apps/new"
        apply_soup = BeautifulSoup(requests.get(apply_url, headers=headers, timeout=10).text, "html.parser")
        description = apply_soup.select_one(".trix-content, .job-description, [class*='description']")

    return {
        "title": title.get_text(strip=True) if title else None,
        "description": description.get_text(strip=True) if description else None,
        "description_html": str(description) if description else None,
        "location": location_info.get_text(strip=True) if location_info else None,
        "url": job_url,
    }

# Fetch details for the first job
if jobs:
    details = fetch_job_details(jobs[0]["url"])
    print(f"Title: {details['title']}")
    print(f"Location: {details['location']}")
    print(f"Description length: {len(details['description'] or '')} chars")

Handle pagination

CareerPlug pages the board with a ?page={n} query parameter. Iterate until a page yields no job container or no new links, and space requests out to stay under the load balancer's throttle.

Step 4: Handle pagination
import time

def fetch_all_jobs(company_slug: str, max_pages: int = 10) -> list:
    """Fetch all jobs from a CareerPlug company with pagination."""
    all_jobs = []
    base_url = f"https://{company_slug}.careerplug.com/jobs"

    for page in range(1, max_pages + 1):
        url = f"{base_url}?page={page}" if page > 1 else base_url
        response = requests.get(url, headers=headers, timeout=10)

        if response.status_code in (403, 404):
            break  # 404 = unknown company; 403 = AWS ELB throttling

        soup = BeautifulSoup(response.text, "html.parser")
        job_container = soup.select_one("#job_table, #job-list")

        if not job_container:
            break

        job_links = job_container.select("div > a[href^='/jobs/']")

        if not job_links:
            break

        for link in job_links:
            title_elem = link.select_one(".job-title .name")
            location_elem = link.select_one(".job-location")

            all_jobs.append({
                "title": title_elem.get_text(strip=True) if title_elem else None,
                "location": location_elem.get_text(strip=True) if location_elem else None,
                "url": f"https://{company_slug}.careerplug.com{link['href']}",
            })

        print(f"Page {page}: Found {len(job_links)} jobs (total: {len(all_jobs)})")
        time.sleep(1)  # Be respectful

    return all_jobs

jobs = fetch_all_jobs("bemobile")
print(f"Total jobs collected: {len(jobs)}")
Common issues
highWrong company subdomain returns a 404

CareerPlug keys each employer to its own subdomain (e.g. sir-speedy-careers.careerplug.com). Confirm the exact subdomain from the company's careers link before scraping; app.* and www.* are not employer boards.

highAWS load balancer returns HTTP 403 under load

Bursts of requests trigger an AWS ELB 403 (body contains '403 Forbidden') rather than a real block. Treat these as rate limiting: back off, retry with jitter, and keep concurrency low (~3 requests).

criticalThe /jobs.js endpoint is disallowed by robots.txt

Do not fetch /jobs.js — it is explicitly disallowed and only returns Rails UJS DOM-replacement JavaScript, not JSON. Parse the standard HTML /jobs page instead.

mediumFull description only appears on the application page

Some tenants leave the /jobs/{id} detail page thin and serve the complete description on /jobs/{id}/apps/new. Fall back to the apply page when the detail body is empty.

lowLocation arrives as a State-City-ZIP string

Locations are formatted like 'NH-Keene-03431'. Split on hyphens to recover state, city, and ZIP for structured storage.

mediumHTML layout varies between tenants

Themes differ across employers, so use fallback selectors: '#job_table, #job-list' for the container and '.trix-content, .job-description, [class*="description"]' for the body.

mediumNo JSON or GraphQL API is available

CareerPlug exposes no REST/GraphQL endpoint; /jobs.json and ?format=json return HTTP 406 and /api/jobs 404s. All job data must be parsed from server-rendered HTML.

Best practices
  1. 1Fetch the server-rendered /jobs page — never /jobs.js, which robots.txt disallows
  2. 2Identify each employer by its careerplug.com subdomain, skipping app.* and www.*
  3. 3Use fallback container selectors (#job_table, #job-list) to survive theme differences
  4. 4Follow /jobs/{id} to /apps/new when the detail description comes back empty
  5. 5Throttle to ~3 concurrent requests with short delays; treat AWS ELB 403s as rate limiting and back off
  6. 6Split the {State}-{City}-{ZIP} location string into structured city, state, and ZIP fields
Or skip the complexity

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

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

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