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.
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.
- Full Job Descriptions
- Salary Ranges & Currency
- Employment Type
- Location & Geo-Coordinates
- Apply URLs
- Posting & Closing Dates
- 01UK employer job tracking
- 02Salary benchmarking
- 03Careers page aggregation
- 04Recruitment market research
How to scrape PostingPanda.
Step-by-step guide to extracting jobs from PostingPanda-powered career pages—endpoints, authentication, and working code.
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}/")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 XMLimport 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 advertsimport 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 []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_runtimeThe 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).
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.
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.
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.
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.
- 1Route by host suffix before choosing an endpoint (Talos vs legacy).
- 2Always set Referer to https://{host}/ on the legacy filter call — it is the only tenant scope.
- 3Sniff XML vs JSON on the legacy response instead of assuming a format.
- 4Parse UK dates day-first with %d/%m/%Y.
- 5Space requests ~250ms apart and back off on HTTP 403 and 429.
- 6Treat an empty legacy array as real only after confirming the PostingPanda runtime fingerprint.
One endpoint. All PostingPanda jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=postingpanda" \
-H "X-Api-Key: YOUR_KEY" 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.