- 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.
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.
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
- 01Insurance Industry Hiring Research
- 02SMB & Agency Job Aggregation
- 03Local Job Boards
- 04Careers Page Monitoring
Trusted by
- FX Insurance Agency
- Hudson United Insurance Services
- Bowker Insurance Group
- District Office of Farmers Insurance
How to scrape IdealTraits.
Step-by-step guide to extracting jobs from IdealTraits-powered career pages—endpoints, authentication, and working code.
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 209import 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")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")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)- 1Validate that the Base64 board segment decodes to a positive decimal business id before requesting anything
- 2Check input#requesturl on every board page and abort when it names a different tenant
- 3Accept a rel=next link only when it targets the same board and increments the page by exactly one
- 4Reject vacancy cards whose data-landingpage points at a different agency slug
- 5Pace requests about three seconds apart, with at most two concurrent detail fetches
- 6Prefer the JobPosting JSON-LD block for the description, dates and employment type
One endpoint. All IdealTraits jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=idealtraits" \
-H "X-Api-Key: YOUR_KEY"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.
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.