All platforms

Kula Jobs API.

Pull every open role from a growth-stage recruiting platform in a single unauthenticated REST call, with full HTML descriptions, structured pay ranges, and office locations already in the payload.

Get API access
Kula
Live
20K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using Kula
Cover GeniusCleverTapWizCommerce
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 Kula.

Data fields
  • Full HTML job descriptions
  • Structured pay ranges
  • Office locations with city, state & country
  • Workplace type (office, remote, hybrid)
  • Department names
  • Employment type
Use cases
  1. 01Growth-Stage Company Job Tracking
  2. 02Tech Talent Sourcing
  3. 03Compensation Benchmarking
  4. 04Job Board Aggregation
Trusted by
Cover GeniusCleverTapWizCommerce
DIY GUIDE

How to scrape Kula.

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

RESTbeginnerNone documented; be polite (~200ms between requests)No auth

Extract the account name from the URL

Every Kula board lives on the shared careers.kula.ai domain, so the company is identified by the first path segment. Parse it out to build API calls.

Step 1: Extract the account name from the URL
import re

def extract_account_name(url: str) -> str:
    """Extract account name from a Kula career page URL."""
    pattern = r'careers\.kula\.ai/([^/]+)'
    match = re.search(pattern, url)
    if match:
        return match.group(1)
    raise ValueError(f"Invalid Kula URL: {url}")

# Example usage
url = "https://careers.kula.ai/covergenius"
account_name = extract_account_name(url)
print(f"Account name: {account_name}")  # Output: covergenius

Fetch job listings from the internal API

Call the internal ats_job_posts endpoint with the account name. The response already contains full HTML descriptions, so no per-job detail request is needed.

Step 2: Fetch job listings from the internal API
import requests

def fetch_kula_jobs(account_name: str, page: int = 1, items: int = 99) -> dict:
    """Fetch a page of jobs from the Kula API."""
    url = "https://careers.kula.ai/api/internal/ats_job_posts"
    params = {
        "accountName": account_name,
        "page": page,
        "type": "ats_job_post.index",
        "items": items,
    }

    response = requests.get(url, params=params, timeout=30)
    response.raise_for_status()
    return response.json()

# Fetch the first page for a company
data = fetch_kula_jobs("covergenius")
meta = data.get("meta", {})
print(f"Found {meta.get('count')} jobs across {meta.get('pages')} page(s)")

Parse the fields you need from each job

Root-level fields carry the id, title and visibility flags; the nested ats_job object holds the description, department, workplace type, offices and compensation.

Step 3: Parse the fields you need from each job
def parse_job(job: dict, account_name: str) -> dict:
    """Parse a single job from the Kula API response."""
    ats_job = job.get("ats_job", {}) or {}

    # Prefer the full location string; fall back to Remote when no offices.
    offices = ats_job.get("offices", []) or []
    location = offices[0].get("location", "") if offices else "Remote"

    base_salary = (ats_job.get("compensation", {}) or {}).get("base_salary", {}) or {}

    return {
        "id": job.get("id"),
        "title": job.get("title"),
        "department": (ats_job.get("ats_department", {}) or {}).get("name"),
        "location": location,
        "workplace_type": ats_job.get("workplace"),      # office, remote, hybrid
        "employment_type": ats_job.get("employment_type"),
        "salary_min": base_salary.get("min_amount"),
        "salary_max": base_salary.get("max_amount"),
        "salary_currency": base_salary.get("currency"),
        "description_html": ats_job.get("job_description"),
        "is_listed": job.get("listed", False),
        "is_confidential": job.get("is_confidential", False),
        "url": f"https://careers.kula.ai/{account_name}/{job.get('id')}",
    }

# Parse every job on the page
for job in data.get("data", []):
    parsed = parse_job(job, "covergenius")
    print(f"{parsed['title']} - {parsed['location']}")

Follow pagination via meta.pages

The meta block reports the total number of pages. Loop from page 1 until the current page reaches meta.pages to collect the full list.

Step 4: Follow pagination via meta.pages
def fetch_all_kula_jobs(account_name: str) -> list:
    """Fetch all jobs across every page."""
    all_jobs = []
    page = 1

    while True:
        data = fetch_kula_jobs(account_name, page=page)
        all_jobs.extend(data.get("data", []))

        total_pages = data.get("meta", {}).get("pages", 1)
        if page >= total_pages:
            break
        page += 1

    return all_jobs

all_jobs = fetch_all_kula_jobs("covergenius")
print(f"Total jobs fetched: {len(all_jobs)}")

Filter to public, non-confidential roles

Keep only jobs flagged listed=true and is_confidential=false, mirroring what candidates actually see on the public board.

Step 5: Filter to public, non-confidential roles
def filter_active_jobs(jobs: list) -> list:
    """Keep only publicly listed, non-confidential jobs."""
    return [
        job for job in jobs
        if job.get("listed") is True and job.get("is_confidential") is False
    ]

active_jobs = filter_active_jobs(fetch_all_kula_jobs("covergenius"))
print(f"Active public jobs: {len(active_jobs)}")
Common issues
highCompany not found (HTTP 404)

The account name must match the first path segment of careers.kula.ai/{accountName} exactly. Re-derive it from the live board URL rather than guessing.

mediumRequests blocked or throttled (HTTP 403 or 429)

Treat 403 and 429 as rate limiting: pause page requests roughly 200ms apart and apply exponential backoff before retrying rather than hammering the endpoint.

mediumAPI returns an empty data array

The company may simply have no open roles, or the account name may be wrong. Check meta.count and confirm the board loads in a browser before assuming a scraper fault.

lowMissing description, offices, or compensation fields

job_description, offices and compensation can be absent on some roles. Default nested lookups to empty dict/list and null-check before use to avoid KeyError crashes.

highUndocumented internal endpoint may change

/api/internal/ats_job_posts is an internal endpoint with no public contract. Validate the response shape (data plus meta) on each run and alert when the structure shifts.

Best practices
  1. 1Call the JSON API instead of scraping HTML for reliable, structured data
  2. 2Request 99 items per page to minimize pagination round-trips
  3. 3Skip jobs where listed is false or is_confidential is true
  4. 4Iterate the offices array to capture every location on multi-site roles
  5. 5Space page requests ~200ms apart and back off on HTTP 403/429
  6. 6Cache results and refresh daily, since boards update infrequently
Or skip the complexity

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

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

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