All platforms

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.

Get API access
Amris
Live
<3haverage discovery time
1hrefresh interval
Companies using Amris
J D WetherspoonGreenSquareAccordTenon FM
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 Amris.

Data fields
  • Job Titles
  • Salary Ranges
  • Contract Types
  • Closing Dates
  • Locations & Remote Flags
  • Full Job Descriptions
Use cases
  1. 01UK Recruitment Monitoring
  2. 02Salary Benchmarking
  3. 03Job Aggregation
  4. 04Vacancy Alerting
Trusted by
J D WetherspoonGreenSquareAccordTenon FM
DIY GUIDE

How to scrape Amris.

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

HTMLintermediate~200ms between requests; max 3 concurrent detail fetches (observed pacing)No auth

Fetch the tenant's vacancy list

Every Amris employer sits on its own {tenant}.amris-wizard-proxy.com host. Request /vacancyList.php, the server-rendered listing fragment that holds the full current table — do not use /createMarkerScript.php, which returns executable JavaScript rather than structured data.

Step 1: Fetch the tenant's vacancy list
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.text

Parse the vacancy table

Read rows from table.searchResultsMain and keep only those whose job-title link resolves to a numeric /view/{id} or /jobs/view/{id} path. The row cells expose location, salary, contract type, and closing date.

Step 2: Parse the vacancy table
import 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")

Detect empty boards and unfinished pages

When the table is absent, distinguish a genuinely empty board from a layout change by checking for an explicit no-vacancies message. Treat the snapshot as complete only when no next-page control is advertised.

Step 3: Detect empty boards and unfinished pages
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")

Hydrate job details

Fetch each /view/{id} page and read the labelled .viewLabel fields (Job Ref, Salary, Contract type, Department, Location, Closing date). Pull the description from #data_primaryInformation (falling back to .viewDataFD), and reject the record if its Job Ref disagrees with the route id.

Step 4: Hydrate job details
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]
Common issues
criticalScraping /createMarkerScript.php instead of the listing fragment

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.

highsearchResultsMain table is missing from the response

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.

highDetail Job Ref disagrees with the /view/{id} route id

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.

mediumPagination undercounts vacancies

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.

lowBroken detail links from placeholder tokens

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.

Best practices
  1. 1Always fetch /vacancyList.php; never scrape /createMarkerScript.php (it is executable JS, not data).
  2. 2Derive the tenant from the first label of the *.amris-wizard-proxy.com host.
  3. 3Accept only rows whose href yields a numeric /view/{id} or /jobs/view/{id}, and de-dupe by requirement id.
  4. 4Verify the detail Job Ref matches the route id before trusting a record.
  5. 5Stop treating a page as complete once a rel='next' control appears.
  6. 6Pace requests to ~200ms with no more than 3 concurrent detail fetches, and parse dates as UK/en-GB.
Or skip the complexity

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

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

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.

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