All platforms

PostingPanda Jobs API.

Pull complete job snapshots — full descriptions, salary bands, and apply links — from UK careers sites running on PostingPanda and its newer Talos360 platform.

Get API access
PostingPanda
Live
<3haverage discovery time
1hrefresh interval
Companies using PostingPanda
Cats ProtectioneasyHotelShoe ZonePizzaExpressBetfred
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 PostingPanda.

Data fields
  • Full Job Descriptions
  • Salary Ranges & Currency
  • Employment Type
  • Location & Geo-Coordinates
  • Apply URLs
  • Posting & Closing Dates
Use cases
  1. 01UK employer job tracking
  2. 02Salary benchmarking
  3. 03Careers page aggregation
  4. 04Recruitment market research
Trusted by
Cats ProtectioneasyHotelShoe ZonePizzaExpressBetfred
DIY GUIDE

How to scrape PostingPanda.

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

HybridintermediateNo published limit; space requests ~250ms apartNo auth

Route by host suffix

PostingPanda ships two API generations under one brand, selected by the careers host. `*.talosats-careers.com` boards use the newer two-step Talos360 API; `*.postingpanda.uk` subdomains and white-label custom domains use the legacy filter API. Decide which path to take before making any request.

Step 1: Route by host suffix
from urllib.parse import urlparse

def resolve_generation(careers_url: str) -> str:
    host = (urlparse(careers_url).hostname or "").lower()
    return "talos" if host.endswith(".talosats-careers.com") else "legacy"

# host is the tenant key across both DNS namespaces
host = (urlparse("https://thescoutassociation.postingpanda.uk/").hostname or "").lower()
generation = resolve_generation(f"https://{host}/")

Fetch the legacy generation with a Referer-scoped GET

The legacy endpoint scopes the tenant purely by the Referer header set to the careers host — there is no company id in the URL. The single GET returns the full live-advert set (unpaginated) as DataContract XML on current deployments, or JSON on older ones, so sniff the first non-whitespace byte.

Step 2: Fetch the legacy generation with a Referer-scoped GET
import requests

LEGACY_FILTER_URL = (
    "https://postingpandaapi-live.azurewebsites.net"
    "/api/liveadverts/filter/-in-"
    "?country=United+Kingdom&distanceMiles=10"
    "&extraDataFilters=&location=&searchTerm="
)

def fetch_legacy(host: str) -> list[dict]:
    resp = requests.get(
        LEGACY_FILTER_URL,
        headers={"Referer": f"https://{host}/"},  # the only tenant scope
        timeout=15,
    )
    resp.raise_for_status()
    body = resp.text.lstrip()
    if body.startswith("["):
        return resp.json()               # older deployments emit JSON
    return parse_legacy_xml(resp.text)   # current deployments emit XML

Parse the legacy DataContract XML

The XML root is an `ArrayOf...` wrapper whose children carry `...ResultsQueryFilterLiveAdverts` in their tag name. Namespaces vary, so match on the local name only and read PascalCase fields (AdvertId, JobTitle, JobDescription, SalaryDisplay, ApplyUrl).

Step 3: Parse the legacy DataContract XML
import xml.etree.ElementTree as ET

def _local(tag: str) -> str:
    return tag.rsplit("}", 1)[-1]

def parse_legacy_xml(xml_text: str) -> list[dict]:
    root = ET.fromstring(xml_text)
    if not _local(root.tag).startswith("ArrayOf"):
        return []
    adverts = []
    for item in root:
        if "ResultsQueryFilterLiveAdverts" not in _local(item.tag):
            continue
        fields = {_local(c.tag): (c.text or "").strip() for c in item}
        adverts.append(fields)   # PascalCase keys, same as the JSON path
    return adverts

Fetch the Talos generation via config then search

Newer boards need two calls: GET the public site-config keyed by full host to obtain the native site `obfuscatedId`, then POST it to `vacancies/search`. The response is a complete unpaginated snapshot under `careersSiteVacancies` with camelCase fields (jobPostId, jobTitle, salaryFrom/salaryTo).

Step 4: Fetch the Talos generation via config then search
import requests

TALOS_BASE = "https://api-careers-sites.talos360.com"

def fetch_talos(host: str) -> list[dict]:
    cfg = requests.get(
        f"{TALOS_BASE}/api/careerssites/site/config/get",
        params={"host": host},
        timeout=15,
    ).json()
    obfuscated_id = (cfg.get("siteConfig") or {}).get("obfuscatedId")
    if not obfuscated_id:
        raise ValueError(f"No obfuscatedId for {host}")

    resp = requests.post(
        f"{TALOS_BASE}/api/careerssite/vacancies/search",
        headers={"Origin": f"https://{host}", "Referer": f"https://{host}/"},
        json={
            "careersSiteObfuscatedId": obfuscated_id,
            "metadataFilters": [],
            "preFilters": [],
            "siteType": "External",
        },
        timeout=15,
    )
    resp.raise_for_status()
    return resp.json().get("careersSiteVacancies") or []

Normalize records and confirm an empty legacy board

Both generations build the listing URL as `https://{host}/job/{id}` (AdvertId for legacy, jobPostId for Talos). A legacy empty array is ambiguous — a wrong Referer looks identical to a genuinely empty board — so treat it as authoritative only after the careers homepage shows the PostingPanda runtime fingerprint.

Step 5: Normalize records and confirm an empty legacy board
import requests

def normalize(host: str, generation: str, rec: dict) -> dict:
    if generation == "talos":
        job_id, title = rec.get("jobPostId"), rec.get("jobTitle")
        description, salary = rec.get("jobDescription"), rec.get("salaryDisplay")
    else:
        job_id, title = rec.get("AdvertId"), rec.get("JobTitle")
        description, salary = rec.get("JobDescription"), rec.get("SalaryDisplay")
    return {
        "external_id": job_id,
        "title": title,
        "description": description,
        "salary": salary,
        "listing_url": f"https://{host}/job/{job_id}",
    }

def legacy_empty_is_authoritative(host: str) -> bool:
    html = requests.get(
        f"https://{host}/", headers={"Accept": "text/html"}, timeout=15
    ).text
    default_runtime = "/Scripts/Views/Home/default/JobSearch-angular.js" in html
    custom_runtime = (
        "postingpanda.blob.core.windows.net/clientpublicassets/" in html
        and "/Scripts/" in html
    )
    return default_runtime or custom_runtime
Common issues
highLegacy API returns an empty array for both a wrong Referer and a genuinely empty board

The legacy filter endpoint scopes solely by the Referer header. Always send Referer = https://{host}/, and treat [] as authoritative only after fetching the careers homepage and confirming the PostingPanda runtime fingerprint (JobSearch-angular.js or the clientpublicassets blob).

highLegacy responses are XML, not JSON, on current deployments

Sniff the first non-whitespace character: '[' means JSON, '<' means DataContract XML. Parse the XML by local tag name (ArrayOf... wrapper, ...ResultsQueryFilterLiveAdverts children) because the namespaces vary.

criticalChoosing the wrong generation for a host

Route strictly by host suffix. Only *.talosats-careers.com uses the two-step Talos360 config+search API; *.postingpanda.uk subdomains and verified white-label domains use the legacy Referer-scoped filter GET.

mediumUK expiry dates misparse as month-first

Legacy adverts carry the closing date as an 'Expiry_dd/MM/yyyy' token inside ExtraData. Parse it explicitly with the day-first format ('%d/%m/%Y') instead of relying on locale-default parsing.

lowSending pagination parameters

Both APIs return the tenant's complete snapshot in a single response and reject continuation cursors. Make one call per board and do not add page or offset params.

Best practices
  1. 1Route by host suffix before choosing an endpoint (Talos vs legacy).
  2. 2Always set Referer to https://{host}/ on the legacy filter call — it is the only tenant scope.
  3. 3Sniff XML vs JSON on the legacy response instead of assuming a format.
  4. 4Parse UK dates day-first with %d/%m/%Y.
  5. 5Space requests ~250ms apart and back off on HTTP 403 and 429.
  6. 6Treat an empty legacy array as real only after confirming the PostingPanda runtime fingerprint.
Or skip the complexity

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

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

Access PostingPanda
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