AppOne RSS Jobs API.

AppOne serves each employer a portal at jobs.appone.com/{slug}. One anonymous JSON call returns the whole portal inventory, and a second returns the full description and salary band for any job. Portals that publish RSS instead are served separately.

Get API access

What's in every response.

Data fields, real-world applications, and the companies already running on AppOne RSS.

Data fields

  • Full Job Descriptions
  • Salary Minimum & Maximum
  • Employment & Workplace Type
  • Location Strings
  • Posted Dates
  • Portal-to-Job Backlinks

Use cases

  1. 01SMB Job Aggregation
  2. 02Staffing Agency Feeds
  3. 03Compensation Benchmarking
  4. 04ATS Data Pipelines
DIY GUIDE

How to scrape AppOne RSS.

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

API type
REST
Difficulty
beginner
Rate limit
No published limit; 403/429 under load — ~500ms between requests, max 2 concurrent detail fetches
Authentication
No auth

Resolve the portal slug

An AppOne board is exactly one path segment on jobs.appone.com — sometimes a name like aacnnursing, sometimes a bare number. Anything with a second segment is not a portal. Job pages live on a different host, apply.appone.com/job/{jobId}, where the ID is a 24-character hex string.

Step 1: Resolve the portal slug
import re
from urllib.parse import urlparse

PORTAL_HOST = "jobs.appone.com"
APPLY_HOST = "apply.appone.com"
MONGO_ID = re.compile("^[a-f0-9]{24}$", re.IGNORECASE)

def parse_portal(url: str) -> str | None:
    parsed = urlparse(url)
    if parsed.scheme != "https" or parsed.netloc.lower() != PORTAL_HOST:
        return None
    segments = [s for s in parsed.path.split("/") if s]
    return segments[0].lower() if len(segments) == 1 else None

def parse_job_id(url: str) -> str | None:
    parsed = urlparse(url)
    if parsed.scheme != "https" or parsed.netloc.lower() != APPLY_HOST:
        return None
    segments = [s for s in parsed.path.split("/") if s]
    if len(segments) != 2 or segments[0] != "job" or not MONGO_ID.match(segments[1]):
        return None
    return segments[1].lower()

print(parse_portal("https://jobs.appone.com/aacnnursing"))  # 'aacnnursing'

Fetch the whole portal inventory

The portal API returns the employer name and every published job in a single response — there is no cursor, page parameter or total count to walk. Each row already carries the title, location, employment type, salary object and the canonical apply URL.

Step 2: Fetch the whole portal inventory
import requests
from urllib.parse import quote

def fetch_portal(session, slug: str) -> dict:
    url = f"https://{PORTAL_HOST}/api/portal/v1/companyjobposts/{quote(slug)}"
    resp = session.get(url, headers={"Accept": "application/json"}, timeout=30)
    resp.raise_for_status()
    return resp.json()

session = requests.Session()
portal = fetch_portal(session, "aacnnursing")
print(portal.get("companyName"), len(portal.get("jobPosts") or []))

Map the portal rows

Each row's jobPostUrl points at apply.appone.com/job/{jobPostId}. Confirm the ID in the URL matches the row's jobPostId before you emit it — a disagreement means the row is stale or cross-linked. The salary object exposes minimum, maximum, salaryOption and periodType.

Step 3: Map the portal rows
def map_rows(portal: dict) -> list[dict]:
    company_name = (portal.get("companyName") or "").strip()
    rows = []
    for job in portal.get("jobPosts") or []:
        url = (job.get("jobPostUrl") or "").strip()
        job_id = parse_job_id(url)
        if not job_id or job_id != (job.get("jobPostId") or "").lower():
            continue  # URL and ID disagree — skip the row

        salary = job.get("salary") or {}
        rows.append({
            "id": job_id,
            "title": (job.get("jobTitle") or "").strip(),
            "company": company_name,
            "url": url,
            "location": job.get("location"),
            "employment_type": job.get("jobType"),
            "workplace_type": job.get("workplaceType"),
            "posted_at": job.get("datePosted"),
            "salary_min": salary.get("minimum"),
            "salary_max": salary.get("maximum"),
            "salary_option": salary.get("salaryOption"),
            "salary_period": salary.get("periodType"),
        })
    return rows

rows = map_rows(portal)

Fetch the full description per job

Portal rows carry no body text. The job posting endpoint on apply.appone.com returns the description together with the company name, client ID and the jobPortalUrl that names the owning portal. Verify the returned jobPostId echoes the one you asked for.

Step 4: Fetch the full description per job
import time

def fetch_job(session, job_id: str) -> dict | None:
    url = f"https://{APPLY_HOST}/api/apply/v2/jobposting/{job_id}"
    resp = session.get(url, headers={"Accept": "application/json"}, timeout=30)
    if resp.status_code in (404, 410):
        return None  # posting removed
    resp.raise_for_status()

    job = resp.json() or {}
    if (job.get("jobPostId") or "").lower() != job_id:
        raise RuntimeError("AppOne returned a different job than requested")

    salary = job.get("salary") or {}
    return {
        "id": job_id,
        "title": (job.get("jobTitle") or "").strip(),
        "description_html": (job.get("description") or "").strip(),
        "company": job.get("companyName"),
        "client_id": job.get("clientId"),
        "location": job.get("location"),
        "employment_type": job.get("jobType"),
        "workplace_type": job.get("workplaceType"),
        "portal_url": job.get("jobPortalUrl"),
        "salary_min": salary.get("minimum"),
        "salary_max": salary.get("maximum"),
        "url": f"https://{APPLY_HOST}/job/{job_id}",
    }

for row in rows[:3]:
    print(fetch_job(session, row["id"]))
    time.sleep(0.5)

Recover the portal from a bare job link

Syndicated AppOne links are usually apply.appone.com/job/{id} with no portal in sight. The job posting response carries jobPortalUrl, which points straight back at jobs.appone.com/{slug} — use it to attribute an orphan job to the right employer.

Step 5: Recover the portal from a bare job link
def resolve_portal_from_job(session, job_url: str) -> str | None:
    job_id = parse_job_id(job_url)
    if not job_id:
        return None

    job = fetch_job(session, job_id)
    if not job or not job.get("portal_url"):
        return None
    return parse_portal(job["portal_url"])

print(resolve_portal_from_job(
    session, "https://apply.appone.com/job/000000000000000000000000"))
Common issues
highThe job ID is rejected as malformed
AppOne job IDs are 24-character lowercase hex strings, not integers or slugs. Validate against that shape before calling the posting endpoint; anything else is a marketing or apply-flow URL and will return an error page rather than JSON.
mediumThere is no total count or pagination to verify completeness
The portal endpoint returns the entire inventory in one response and publishes no authoritative total. Use the length of jobPosts as the count, and treat a sudden drop between runs as a signal to re-fetch rather than as immediate evidence that jobs closed.
mediumA bare apply.appone.com link cannot be attributed to an employer
Job URLs carry no portal slug. Call /api/apply/v2/jobposting/{id} and read jobPortalUrl, which names the owning jobs.appone.com portal, before writing the record. Without that step every syndicated link lands in an unattributed bucket.
lowSalary fields are missing on most rows
The salary object is optional and frequently absent or partially filled, with only salaryOption set. Read minimum, maximum, salaryOption and periodType defensively and store nulls rather than coercing a missing band to zero.
Best practices
  1. 1Validate job IDs against the 24-character hex shape before calling the posting API
  2. 2Cross-check each row's jobPostId against the ID inside its jobPostUrl
  3. 3Use jobPortalUrl to attribute an orphan apply.appone.com link to its employer
  4. 4Confirm the response URL was not redirected off the canonical API path
  5. 5Throttle to ~500ms between requests and cap concurrent detail fetches at two
  6. 6Store the salary object's four fields separately instead of flattening to one string
Or skip the complexity

One endpoint. All AppOne RSS jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=appone rss" \
  -H "X-Api-Key: YOUR_KEY"
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.

Ready to integrate

Access AppOne RSS
job data today.

One API call. Structured data. No scraping infrastructure to build or maintain — start with the $5 free starting balance and scale as you grow.

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