IdealTraits Jobs API.

Read hiring from the thousands of independent insurance agencies that run IdealTraits, whose careers boards are server-rendered pages backed by canonical JobPosting JSON-LD on every posting.

Get API access

What's in every response.

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

Data fields

  • Full Job Descriptions
  • JobPosting JSON-LD
  • Employment Type
  • City & State Locations
  • Posted Dates
  • Paginated Agency Boards

Use cases

  1. 01Insurance Industry Hiring Research
  2. 02SMB & Agency Job Aggregation
  3. 03Local Job Boards
  4. 04Careers Page Monitoring

Trusted by

  • FX Insurance Agency
  • Hudson United Insurance Services
  • Bowker Insurance Group
  • District Office of Farmers Insurance
DIY GUIDE

How to scrape IdealTraits.

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

API type
HTML
Difficulty
advanced
Rate limit
Boards advertise a 20-request window; keep to ~1 request every 3 seconds with at most 2 concurrent detail fetches
Authentication
No auth

Decode the board URL into agency slug and business id

An IdealTraits careers board is app.idealtraits.com/{agency-slug}/{businessId}/careers, where the business id is a Base64 string that decodes to a positive decimal id. Validate the decode before requesting anything — a segment that does not decode to a plain number is not an IdealTraits tenant.

Step 1: Decode the board URL into agency slug and business id
import base64
import re
from urllib.parse import urlparse

HOST = "app.idealtraits.com"
SLUG = re.compile(r"^[A-Za-z0-9&,.:-]{1,100}$")
BASE64_ID = re.compile(r"^[A-Za-z0-9+/]+={0,2}$")

def decode_business_id(token: str) -> str | None:
    if not (3 <= len(token) <= 16) or not BASE64_ID.match(token):
        return None
    try:
        decoded = base64.b64decode(token).decode("ascii")
    except Exception:
        return None
    # Must decode to a positive decimal business id with no leading zero.
    if not decoded.isdigit() or decoded.startswith("0") or len(decoded) > 10:
        return None
    return decoded

def parse_board(url: str) -> tuple[str, str] | None:
    parsed = urlparse(url)
    if parsed.scheme != "https" or parsed.netloc.lower() != HOST:
        return None
    parts = parsed.path.strip("/").split("/")
    if len(parts) != 3 or parts[2].lower() != "careers" or not SLUG.match(parts[0]):
        return None
    return (parts[0], parts[1]) if decode_business_id(parts[1]) else None

slug, token = parse_board("https://app.idealtraits.com/Bowker-Insurance-Group/MjA5/careers")
print(slug, token, decode_business_id(token))  # Bowker-Insurance-Group MjA5 209

Confirm the board is the tenant you asked for

Each board embeds its own canonical address in a hidden input#requesturl. Compare that value with the URL you requested before mapping a single card; if it names a different business id, you have followed a redirect onto another agency's board and should stop rather than file its jobs under the wrong employer.

Step 2: Confirm the board is the tenant you asked for
import requests
from bs4 import BeautifulSoup

def board_url(slug: str, token: str, page: int = 1) -> str:
    base = f"https://{HOST}/{slug}/{token}/careers"
    return base if page <= 1 else f"{base}?page={page}"

def fetch_board(session: requests.Session, slug: str, token: str, page: int = 1):
    response = session.get(
        board_url(slug, token, page),
        headers={"Accept": "text/html,application/xhtml+xml"},
        timeout=30,
    )
    response.raise_for_status()
    soup = BeautifulSoup(response.text, "html.parser")

    proof = soup.select_one("input#requesturl")
    proved = parse_board((proof.get("value") if proof else "") or "")
    if proved != (slug, token):
        raise RuntimeError("IdealTraits board did not prove the requested tenant")
    return soup

session = requests.Session()
soup = fetch_board(session, "Bowker-Insurance-Group", "MjA5")

Map the vacancy cards and follow rel=next

Vacancies are .opportunity-card elements whose data-landingpage attribute holds the detail URL, shaped /career/{agency-slug}/{jobId}CPG. Pagination is an ordinary a[rel=next] link; accept it only when it points at the same board and increments the page by exactly one, and treat a board that renders the opportunities section with no cards as a genuinely empty board.

Step 3: Map the vacancy cards and follow rel=next
import time
from urllib.parse import urljoin, parse_qs

DETAIL = re.compile(r"^/career/([A-Za-z0-9&,.:-]+)/([1-9][0-9]{0,9}CPG)$", re.IGNORECASE)

def parse_cards(soup, slug: str) -> list[dict]:
    rows = []
    for card in soup.select(".opportunity-card[data-landingpage]"):
        target = urljoin(f"https://{HOST}/", card["data-landingpage"])
        match = DETAIL.match(urlparse(target).path)
        if not match or match.group(1).lower() != slug.lower():
            continue  # a card that leaves this agency is never this board's job
        title = " ".join((card.select_one("h5").get_text() if card.select_one("h5") else "").split())
        if not title:
            continue
        node = card.select_one("span.text-right") or card.select_one("span.d-block")
        rows.append({
            "id": match.group(2),
            "title": title,
            "listing_url": f"https://{HOST}/career/{match.group(1)}/{match.group(2)}",
            "location": " ".join(node.get_text().split()) if node else None,
        })
    return rows

def crawl_board(session: requests.Session, slug: str, token: str) -> list[dict]:
    page, listings = 1, []
    while True:
        soup = fetch_board(session, slug, token, page)
        cards = parse_cards(soup, slug)
        if not cards and not soup.select_one(".opportunities-section"):
            raise RuntimeError("IdealTraits board did not contain its vacancy collection")
        listings.extend(cards)

        nxt = soup.select_one("a[rel=next]")
        if not nxt or not nxt.get("href"):
            return listings
        query = parse_qs(urlparse(urljoin(board_url(slug, token), nxt["href"])).query)
        if int((query.get("page") or ["0"])[0]) != page + 1:
            raise RuntimeError("IdealTraits exposed an invalid next-page route")
        page += 1
        time.sleep(3.1)  # the board advertises a 20-request window

listings = crawl_board(session, "Bowker-Insurance-Group", "MjA5")
print(f"{len(listings)} vacancies")

Hydrate each posting from its JobPosting JSON-LD

Detail pages carry canonical JobPosting JSON-LD with the full description, dates, and employment type. Re-check the 'View All Job Openings' link on the same page — it must resolve to exactly one board for this agency, which is what lets you attribute a bare /career/... link that arrived without a business id.

Step 4: Hydrate each posting from its JobPosting JSON-LD
import json

def find_job_posting(soup) -> dict | None:
    for tag in soup.find_all("script", type="application/ld+json"):
        try:
            data = json.loads(tag.string or "")
        except (json.JSONDecodeError, TypeError):
            continue
        for node in (data if isinstance(data, list) else [data]):
            if isinstance(node, dict) and node.get("@type") == "JobPosting":
                return node
    return None

def owning_board(soup, slug: str) -> tuple[str, str] | None:
    # Exactly one same-agency board link may be present, otherwise identity is unproved.
    boards = {
        parse_board(urljoin(f"https://{HOST}/", a["href"]))
        for a in soup.select("a[href]")
    }
    boards = {b for b in boards if b and b[0].lower() == slug.lower()}
    return boards.pop() if len(boards) == 1 else None

def fetch_detail(session: requests.Session, listing: dict, slug: str) -> dict | None:
    response = session.get(listing["listing_url"], timeout=30)
    if response.status_code in (404, 410):
        return None  # canonical removal evidence
    response.raise_for_status()
    soup = BeautifulSoup(response.text, "html.parser")

    if owning_board(soup, slug) is None:
        raise RuntimeError("IdealTraits detail page omitted its tenant proof")
    posting = find_job_posting(soup)
    if posting is None:
        raise RuntimeError("IdealTraits detail page carried no JobPosting JSON-LD")

    return {
        **listing,
        "title": posting.get("title") or listing["title"],
        "description_html": posting.get("description"),
        "posted_at": posting.get("datePosted"),
        "valid_through": posting.get("validThrough"),
        "employment_type": posting.get("employmentType"),
        "company": (posting.get("hiringOrganization") or {}).get("name"),
    }

for listing in listings[:3]:
    job = fetch_detail(session, listing, "Bowker-Insurance-Group")
    if job:
        print(job["title"], "-", job["company"])
    time.sleep(3.1)
Common issues
highA /career/... link names no business id
Detail URLs carry the agency slug and the job id but never the Base64 business id, so they cannot be attributed offline. Fetch the detail page and take the single same-agency board link from it; if the page exposes zero or more than one candidate board, leave the job unattributed rather than guessing.
highRequests start failing after a short burst
Boards advertise a 20-request limit, and unthrottled crawling trips it quickly. Keep roughly three seconds between requests and no more than two concurrent detail fetches; on a 403 or 429, back off before retrying rather than rotating straight into another request.
mediumA page with no vacancy cards looks like a dead agency
Distinguish the two cases: a document that still renders the .opportunities-section but no .opportunity-card elements is a genuinely empty board, while a document missing that section entirely is a parse failure or a redirect. Only the first should ever be recorded as zero open jobs.
mediumThe board id fails to decode
The second path segment is Base64 and must decode to a positive decimal business id with no leading zero. URL-encoded or truncated tokens decode to junk; validate the decode up front so a malformed link fails immediately instead of producing an unrelated agency's board.
Best practices
  1. 1Validate that the Base64 board segment decodes to a positive decimal business id before requesting anything
  2. 2Check input#requesturl on every board page and abort when it names a different tenant
  3. 3Accept a rel=next link only when it targets the same board and increments the page by exactly one
  4. 4Reject vacancy cards whose data-landingpage points at a different agency slug
  5. 5Pace requests about three seconds apart, with at most two concurrent detail fetches
  6. 6Prefer the JobPosting JSON-LD block for the description, dates and employment type
Or skip the complexity

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

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=idealtraits" \
  -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 IdealTraits
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