All platforms

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.

Get API access
Tribepad
Live
<3haverage discovery time
1hrefresh interval
Companies using Tribepad
GreggsSubwaySagaHandelsbankenTwinkl
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 Tribepad.

Data fields
  • Full Job Descriptions
  • Salary & Contract Details
  • Location & Closing Dates
  • Department & Business Unit
  • Job Reference Codes
  • Native Apply URLs
Use cases
  1. 01UK Retail & Care Hiring Feeds
  2. 02High-Volume Vacancy Aggregation
  3. 03Public Sector Job Monitoring
  4. 04White-Label Careers Sync
Trusted by
GreggsSubwaySagaHandelsbankenTwinklCard Factory
DIY GUIDE

How to scrape Tribepad.

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

HTMLintermediateNo official limit; self-throttle to ~2 req/sec (429/403 signal blocking)No auth

Resolve the board origin

Tribepad tenants live either on a vendor subdomain (twinkl.tribepad.com, acme.tribepad.io) or on a customer-owned white-label domain (careerssearch.greggs.co.uk). All requests hang off the board's scheme + host, so derive the origin first.

Step 1: Resolve the board origin
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.com

Detect the board generation and fetch v2 listings

Each tenant runs exactly one of two mutually-exclusive frontends. Probe the v2 (Laravel-Livewire) board first at /v2/job/search; a 404 or 410 means the tenant is a classic-generation board instead. v2 cards are a.tbp-li-title anchors that link to /members/modules/job/detail.php?record={id}.

Step 2: Detect the board generation and fetch v2 listings
import 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 jobs

Paginate through the v2 board

v2 boards return 12 cards per page with a '{N} Search Results' total and 1-based ?page=N links. Walk pages until one adds no new records, deduplicating on the native record ID, and cap at ~50 pages to bound runaway pagination.

Step 3: Paginate through the v2 board
RESULTS_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_jobs

Fall back to the classic board

Classic-generation tenants serve /jobs/search with /jobs/search/-1/{page} pagination (category -1 = all categories). Cards are anchors whose href contains /jobs/job/{slug}/{id}, and the trailing numeric segment is the native job ID.

Step 4: Fall back to the classic board
CLASSIC_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_jobs

Fetch job details (JSON-LD vs HTML table)

Classic detail pages embed a JSON-LD JobPosting with description, employment type and dates. v2 detail pages carry no JSON-LD — read the advert body from div.job-advert-content and the label/value rows from .job_details_table. Branch on which is present.

Step 5: Fetch job details (JSON-LD vs HTML table)
import 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"),
    }
Common issues
highAssuming a single board layout — the wrong path 404s

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.

highJSON-LD parsing returns nothing on v2 detail pages

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.

mediumNot every board lives on *.tribepad.com

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.

mediumPagination loops or duplicates jobs

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.

mediumRequests start returning HTTP 403 or 429

Aggressive crawling triggers 403 (blocked) or 429 (rate limited). Throttle to roughly two requests per second and keep concurrent detail fetches around three.

Best practices
  1. 1Probe /v2/job/search first, then fall back to /jobs/search — each tenant serves only one generation.
  2. 2Branch detail parsing on JSON-LD: classic boards expose a JobPosting, v2 boards need HTML selectors.
  3. 3Deduplicate on the native record/job ID and stop paging when a page adds nothing new.
  4. 4Pass location_country=1 on v2 boards to return vacancies across all countries.
  5. 5Throttle to ~2 requests/second and cap concurrent detail fetches at ~3 to avoid 403/429.
  6. 6Cache boards daily — high-volume UK retail and care vacancies churn but rarely intra-day.
Or skip the complexity

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

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

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.

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