Tribepad Jobs API.
Pull complete vacancy feeds from UK high-volume employers in retail, care and the public sector. Every board renders full HTML on a cold request, so no login or headless browser is needed — just paginate and parse.
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 Tribepad.
- Full Job Descriptions
- Salary & Contract Details
- Location & Closing Dates
- Department & Business Unit
- Job Reference Codes
- Native Apply URLs
- 01UK Retail & Care Hiring Feeds
- 02High-Volume Vacancy Aggregation
- 03Public Sector Job Monitoring
- 04White-Label Careers Sync
How to scrape Tribepad.
Step-by-step guide to extracting jobs from Tribepad-powered career pages—endpoints, authentication, and working code.
import requests
from urllib.parse import urlparse
# Vendor board: {tenant}.tribepad.com / .tribepad.io
# White-label board: a customer's own careers domain
board_url = "https://twinkl.tribepad.com"
parts = urlparse(board_url)
origin = f"{parts.scheme}://{parts.netloc}"
print(origin) # https://twinkl.tribepad.comimport re
from bs4 import BeautifulSoup
RECORD_RE = re.compile(r"[?&]record=(\d+)", re.I)
def fetch_v2_page(origin, page):
url = f"{origin}/v2/job/search?location_country=1&page={page}"
resp = requests.get(url, timeout=15)
if resp.status_code in (404, 410):
return None # not a v2 board — fall back to classic
resp.raise_for_status()
return resp.text
def parse_v2_cards(html, origin):
soup = BeautifulSoup(html, "html.parser")
jobs = []
for a in soup.select("a.tbp-li-title"):
m = RECORD_RE.search(a.get("href", ""))
if not m:
continue
jobs.append({
"external_id": m.group(1),
"title": " ".join(a.get_text().split()),
"detail_url": requests.compat.urljoin(origin + "/", a["href"]),
})
return jobsRESULTS_TOTAL_RE = re.compile(r"(\d[\d,]*)\s+Search\s+Results", re.I)
def scrape_v2(origin, max_pages=50):
html = fetch_v2_page(origin, 1)
if html is None:
return None # tenant is a classic board, not v2
total = RESULTS_TOTAL_RE.search(BeautifulSoup(html, "html.parser").get_text())
print("advertised total:", total.group(1) if total else "unknown")
all_jobs, seen = [], set()
for page in range(1, max_pages + 1):
if page > 1:
html = fetch_v2_page(origin, page)
new = [j for j in parse_v2_cards(html, origin) if j["external_id"] not in seen]
if not new: # empty or repeated page -> board exhausted
break
seen.update(j["external_id"] for j in new)
all_jobs.extend(new)
return all_jobsCLASSIC_ID_RE = re.compile(r"/jobs/job/[^/]+/(\d+)", re.I)
def scrape_classic(origin, max_pages=50):
first = requests.get(f"{origin}/jobs/search", timeout=15)
if first.status_code in (404, 410):
return None
first.raise_for_status()
all_jobs, seen, html = [], set(), first.text
for page in range(1, max_pages + 1):
if page > 1:
r = requests.get(f"{origin}/jobs/search/-1/{page}", timeout=15)
if r.status_code in (404, 410):
break
r.raise_for_status()
html = r.text
soup = BeautifulSoup(html, "html.parser")
new = []
for a in soup.select("a[href*='/jobs/job/']"):
m = CLASSIC_ID_RE.search(a.get("href", ""))
if not m or m.group(1) in seen:
continue
title_el = a.select_one(".job-list-title") or a
new.append({
"external_id": m.group(1),
"title": title_el.get_text(strip=True),
"detail_url": requests.compat.urljoin(origin + "/", a["href"]),
})
if not new:
break
seen.update(j["external_id"] for j in new)
all_jobs.extend(new)
return all_jobsimport json
def fetch_detail(detail_url):
html = requests.get(detail_url, timeout=15).text
soup = BeautifulSoup(html, "html.parser")
# classic detail pages embed a JSON-LD JobPosting
for tag in soup.select('script[type="application/ld+json"]'):
try:
data = json.loads(tag.string or "{}")
except json.JSONDecodeError:
continue
if data.get("@type") == "JobPosting":
return {
"title": data.get("title"),
"description": data.get("description"),
"employment_type": data.get("employmentType"),
"posted_at": data.get("datePosted"),
"closes_at": data.get("validThrough"),
}
# v2 detail pages: structured advert HTML, no JSON-LD
body = soup.select_one("div.job-advert-content") or soup.select_one(".job-advert-content")
fields = {}
for row in soup.select(".job_details_table .row"):
cols = row.select(".col-sm-6")
if len(cols) >= 2:
label = cols[0].get_text(strip=True).rstrip(":").lower()
fields[label] = cols[1].get_text(strip=True)
h1 = soup.select_one("h1")
return {
"title": h1.get_text(strip=True) if h1 else None,
"description": body.decode_contents().strip() if body else None,
"salary": fields.get("salary"),
"contract_type": fields.get("contract type"),
"location": fields.get("location"),
"closing_date": fields.get("closing date"),
}A tenant runs exactly one generation; the other path returns 404/410. Probe /v2/job/search first and fall back to /jobs/search on 404 or 410 rather than hard-coding one layout.
Only classic detail pages carry a JSON-LD JobPosting. v2 pages have none — parse the advert body from div.job-advert-content and the .job_details_table label/value rows instead. Branch on whether a JobPosting script is present.
Many retail and public-sector tenants front Tribepad on their own domain (e.g. careerssearch.greggs.co.uk). Use the board's actual careers URL — a bare /jobs/search path on an unknown host does not prove the site is Tribepad.
Cards repeat once you page past the last page. Track seen record/job IDs, stop when a page adds nothing new, and cap at ~50 pages to avoid runaway loops on large boards.
Aggressive crawling triggers 403 (blocked) or 429 (rate limited). Throttle to roughly two requests per second and keep concurrent detail fetches around three.
- 1Probe /v2/job/search first, then fall back to /jobs/search — each tenant serves only one generation.
- 2Branch detail parsing on JSON-LD: classic boards expose a JobPosting, v2 boards need HTML selectors.
- 3Deduplicate on the native record/job ID and stop paging when a page adds nothing new.
- 4Pass location_country=1 on v2 boards to return vacancies across all countries.
- 5Throttle to ~2 requests/second and cap concurrent detail fetches at ~3 to avoid 403/429.
- 6Cache boards daily — high-volume UK retail and care vacancies churn but rarely intra-day.
One endpoint. All Tribepad jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=tribepad" \
-H "X-Api-Key: YOUR_KEY" Access Tribepad
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.