Amris Jobs API.
Pull structured vacancies from UK employers running Amris-hosted candidate boards, complete with salary, contract type, and closing dates — straight from server-rendered HTML, no JavaScript execution required.
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 Amris.
- Job Titles
- Salary Ranges
- Contract Types
- Closing Dates
- Locations & Remote Flags
- Full Job Descriptions
- 01UK Recruitment Monitoring
- 02Salary Benchmarking
- 03Job Aggregation
- 04Vacancy Alerting
How to scrape Amris.
Step-by-step guide to extracting jobs from Amris-powered career pages—endpoints, authentication, and working code.
import requests
# The tenant is the first label of the *.amris-wizard-proxy.com host.
tenant = "wetherspoon"
base = f"https://{tenant}.amris-wizard-proxy.com/vacancyList.php"
resp = requests.get(base, headers={"Accept": "text/html"}, timeout=15)
resp.raise_for_status()
html = resp.textimport re
from urllib.parse import urljoin
from bs4 import BeautifulSoup
JOB_PATH = re.compile(r"/(?:jobs/)?view/(\d+)(?:/|$)", re.IGNORECASE)
def cell(row, *classes):
for name in classes:
node = row.select_one(f"td.{name}")
if node and node.get_text(strip=True):
return node.get_text(" ", strip=True)
return None
soup = BeautifulSoup(html, "html.parser")
table = soup.select_one("table.searchResultsMain")
jobs, seen = [], set()
for row in table.select("tr") if table else []:
anchor = row.select_one("td.searchTable_jobTitle a[href]")
if not anchor:
continue
# Strip the Amris #QUERY_STRING# placeholder before resolving the link.
href = anchor["href"].replace("?#QUERY_STRING#", "").replace("#QUERY_STRING#", "")
detail_url = urljoin(base, href.strip()).split("#")[0].rstrip("/")
m = JOB_PATH.search(detail_url)
if not m or m.group(1) in seen:
continue # reject non-numeric rows and de-dupe by requirement id
seen.add(m.group(1))
jobs.append({
"external_id": m.group(1),
"title": anchor.get_text(" ", strip=True),
"listing_url": detail_url,
"location": cell(row, "searchTable_location"),
"salary_text": cell(row, "searchTable_salary"),
"contract_type": cell(row, "searchTable_type", "searchTable_jobType"),
"closes": cell(row, "searchTable_closeDate"),
})
print(f"Found {len(jobs)} vacancies")EMPTY = re.compile(r"\b(?:no current vacancies|no vacancies found|0 vacancies)\b", re.IGNORECASE)
if table is None:
if EMPTY.search(soup.get_text(" ")):
print("Board has no current vacancies")
else:
raise RuntimeError("searchResultsMain table missing — layout may have changed or been blocked")
# A next control means more rows exist; do not treat this page as the full set.
has_next = soup.select_one("a[rel='next'][href], .pagination a.next[href]") is not None
if has_next:
print("More pages advertised — snapshot is incomplete")def fetch_details(job):
resp = requests.get(job["listing_url"], headers={"Accept": "text/html"}, timeout=15)
resp.raise_for_status()
doc = BeautifulSoup(resp.text, "html.parser")
fields = {}
for label in doc.select(".viewLabel"):
key = label.get_text(strip=True).rstrip(":")
sib = label.find_next_sibling()
value = sib.get_text(" ", strip=True) if sib else ""
if key and value:
fields[key] = value
ref = fields.get("Job Ref") or fields.get("Reference")
if ref and ref != job["external_id"]:
raise ValueError("Detail Job Ref disagrees with the /view/{id} route")
node = doc.select_one("#data_primaryInformation") or doc.select_one(".viewDataFD")
site = doc.select_one("meta[property='og:site_name']")
return {
**job,
"title": fields.get("Job Title") or fields.get("Position") or job["title"],
"description": node.get_text(" ", strip=True) if node else "",
"department": fields.get("Department") or fields.get("Pub"),
"hours_per_week": fields.get("Hours per week"),
"company": site["content"] if site and site.has_attr("content") else None,
}
detailed = [fetch_details(j) for j in jobs]The marker endpoint returns executable JavaScript containing HTML strings, not a structured jobs API. Always request /vacancyList.php, which renders the full current table server-side.
Before failing, strip the page text and look for an explicit 'no current vacancies' / '0 vacancies' message — that means the board is genuinely empty. Otherwise treat the missing table as a layout change or block and stop, rather than reporting zero jobs.
Never remap the record. A mismatch between the labelled Job Ref and the numeric route id is a parse error; skip the job so a stale or misrouted page cannot overwrite good data.
Only mark a snapshot complete when there is no a[rel='next'] or .pagination a.next control. When a next control is present, follow it (or flag the run as incomplete) so you do not miss rows beyond page one.
Hrefs can carry an Amris '#QUERY_STRING#' / '?#QUERY_STRING#' placeholder. Strip it before resolving the URL, and parse UK dd/mm/yyyy dates as en-GB, treating 'not set' as no date.
- 1Always fetch /vacancyList.php; never scrape /createMarkerScript.php (it is executable JS, not data).
- 2Derive the tenant from the first label of the *.amris-wizard-proxy.com host.
- 3Accept only rows whose href yields a numeric /view/{id} or /jobs/view/{id}, and de-dupe by requirement id.
- 4Verify the detail Job Ref matches the route id before trusting a record.
- 5Stop treating a page as complete once a rel='next' control appears.
- 6Pace requests to ~200ms with no more than 3 concurrent detail fetches, and parse dates as UK/en-GB.
One endpoint. All Amris jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=amris" \
-H "X-Api-Key: YOUR_KEY" Access Amris
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.