# Eluta.ca Jobs Scraper (`memo23/eluta-scraper`) Actor

Scrape eluta.ca — Canada's national job search engine — by keyword and city. Full descriptions, CAD salary min/max, employer website, NAICS code, HQ geo, Top-100 employer rating, and the real ATS apply link (not an eluta redirect). Incremental monitoring built in. JSON or CSV out.

- **URL**: https://apify.com/memo23/eluta-scraper.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Eluta.ca Jobs Scraper — Canadian Jobs, Salaries & Real Apply Links

Turn [eluta.ca](https://www.eluta.ca) — Canada's national job search engine, indexing postings directly from ~10,000 employer career sites — into structured hiring data. Search any keyword × location combination (or paste any eluta URL) and get full job descriptions, structured CAD salaries, employer metadata with NAICS codes and HQ coordinates, and the employer's **real ATS apply link** — not an eluta redirect.

**$1 per 1,000 jobs** ($0.001/result — a third less than the $1.5 category leader), and the only Eluta actor with **real employer-email discovery**: opt-in enrichment that looks up the employer's actual contact addresses instead of regexing the description text and hoping an email was typed into it.

![How Eluta.ca Jobs Scraper works](https://raw.githubusercontent.com/muhamed-didovic/muhamed-didovic.github.io/main/assets/how-it-works-eluta.png)

### Why this actor

- **Real apply URLs.** Every job's outbound redirect is resolved to the employer's actual ATS posting (Workday, iCIMS, Greenhouse, SmartRecruiters, Talcura…) — the link a candidate or recruiter actually needs.
- **Full descriptions, not snippets.** Complete description HTML + plain text from the job page (typically 1,500–6,000 chars), plus the SERP snippet.
- **Salary-first.** Structured CAD salary (`salaryMin` / `salaryMax` / `salaryType`) straight from eluta's own JSON-LD where the employer provides it, with a regex fallback over description text.
- **Employer intelligence built in.** Employer website + domain, logo, description, NAICS industry code, HQ street address with latitude/longitude, and the Canada's Top 100 Employers rating where awarded.
- **$1 per 1,000 jobs.** $0.005 per run + $0.001 per result — priced below the leading Eluta actor, with the same fields and more.
- **Source transparency.** Each row carries the employer career-site domain eluta indexed the posting from (`sourceDomain`) and the employer's ATS job ID (`atsJobId`).
- **Monitoring mode.** Run it on a schedule and only get postings you haven't seen before — no charge for skipped repeats.

### Use cases

| Who | What |
|---|---|
| Recruiters & sourcers | Fresh Canadian postings by keyword/city with direct ATS links and hiring-employer websites |
| Job-market analysts | Salary bands, employment types, NAICS industries and posting velocity across Canada |
| Lead-gen teams | Companies actively hiring (with website, domain, HQ geo) as buying-intent signals — add opt-in contact-email enrichment |
| Job boards & aggregators | Structured Canadian feed with dedupe-friendly stable IDs and change monitoring |
| AI agents / LLM apps | Clean JSON rows with full description text, ready for embedding or summarisation |

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `query` | array | `["software engineer"]` | Each keyword runs as its own search; eluta in-query qualifiers allowed |
| `location` | array | `["Toronto"]` | Combined with every keyword; empty = all of Canada |
| `sortBy` | string | `relevance` | `date` = newest first (best for monitoring), `popularity` |
| `startUrls` | array | — | Any eluta.ca URL: `/search?q=…`, SEO pages (`/Nurse-jobs-in-Toronto-ON`), employer pages (`/jobs-at-…`), direct job URLs (`/spl/…`) |
| `includeDetails` | boolean | `true` | Fetch each job's detail page (description, dates, employer metadata, salary). Off = fast SERP-only rows |
| `resolveApplyUrls` | boolean | `true` | Resolve the outbound redirect to the employer's real ATS URL |
| `enrichEmails` | boolean | `false` | Opt-in employer contact-email discovery (billed per email found) |
| `monitoringMode` | boolean | `false` | Only return jobs not delivered on previous runs |
| `maxItems` | integer | `1000` | Hard cap across all searches |
| `maxPagesPerSearch` | integer | `20` | ~10 jobs per SERP page |
| `maxConcurrency` | integer | `10` | Parallel job-page fetches |

#### Example input

```json
{
    "query": ["registered nurse", "software engineer"],
    "location": ["Toronto", "Vancouver"],
    "sortBy": "date",
    "maxItems": 200
}
```

Monitoring feed (schedule daily; only new postings after the first baseline run):

```json
{
    "query": ["data engineer"],
    "location": ["Toronto"],
    "sortBy": "date",
    "monitoringMode": true
}
```

### Output schema

One row per job. All fields always present — unavailable data points are `null`, never omitted.

#### Example job record (trimmed real row)

```json
{
    "type": "job",
    "jobId": "a69ac728cec9333a7faf711a00cec597",
    "atsJobId": "23804",
    "title": "Registered Nurse Critical Care 6 East Certificate Required",
    "companyName": "Humber River Health",
    "companySlug": "humber-river-health",
    "companyUrl": "https://www.eluta.ca/jobs-at-humber-river-health",
    "companyWebsite": "https://www.hrh.ca/",
    "companyDomain": "hrh.ca",
    "naicsCode": "622110",
    "companyHeadquarters": {
        "streetAddress": "1235 Wilson",
        "addressLocality": "North York",
        "addressRegion": "Ontario",
        "postalCode": "M3M 0B2",
        "addressCountry": "Canada",
        "latitude": 43.7256,
        "longitude": -79.4849
    },
    "topEmployer": false,
    "topEmployerRating": null,
    "location": "North York, ON",
    "postedAgo": "1 hour ago",
    "postedDate": "2026-08-04T03:38:14",
    "closingDate": "2026-09-08T00:00:00",
    "employmentType": "FULL_TIME",
    "industry": "Health/Medical - Nursing",
    "salaryText": "$41.15 - $58.98",
    "salaryMin": 41.15,
    "salaryMax": 58.98,
    "salaryCurrency": "CAD",
    "salaryType": "HOUR",
    "sourceDomain": "careersen-hrrh.icims.com",
    "elutaUrl": "https://www.eluta.ca/spl/registered-nurse-critical-care-6-east-certificate-required-a69ac728cec9333a7faf711a00cec597",
    "applyUrl": "https://careersen-hrrh.icims.com/jobs/23804/registered-nurse-critical-care-6-east-certificate-required/job",
    "applyUrlResolved": true,
    "descriptionText": "Position Profile Humber River Health. Tomorrow's Healthcare, Today…",
    "descriptionHtml": "<p>…</p>",
    "searchQuery": "registered nurse",
    "detailFetched": true,
    "scrapedAt": "2026-08-04T09:28:41.112Z"
}
```

#### Key output fields

| Group | Fields |
|---|---|
| Identity | `jobId` (stable eluta hex id), `atsJobId`, `elutaUrl` |
| Role | `title`, `employmentType`, `industry`, `descriptionText`, `descriptionHtml`, `descriptionSnippet` |
| Salary | `salaryText`, `salaryMin`, `salaryMax`, `salaryCurrency` (CAD), `salaryType` (`YEAR` / `HOUR`) |
| Employer | `companyName`, `companyLegalName`, `companySlug`, `companyUrl`, `companyWebsite`, `companyDomain`, `companyDescription`, `companyLogoUrl`, `naicsCode`, `companyHeadquarters` (address + lat/lng) |
| Recognition | `topEmployer`, `topEmployerRating`, `topEmployerReviewUrl` (Canada's Top 100 Employers) |
| Location | `location`, `addressLocality`, `addressRegion`, `addressCountry` |
| Dates | `postedAgo`, `postedDate` (ISO), `closingDate` (ISO) |
| Apply | `applyUrl` (real ATS link), `applyUrlResolved`, `sourceDomain` |
| Run context | `searchQuery`, `searchUrl`, `isSponsored`, `detailFetched`, `scrapedAt` |

### How it works

1. Your keywords × locations become eluta searches (or your pasted URLs are auto-classified: SERP / SEO page / employer page / direct job).
2. SERP pages are walked via `?pg=N`, ~10 jobs per page, streaming rows into a sliding-window worker pool — first results arrive within seconds.
3. Each job's detail page is fetched and parsed: eluta renders two page variants (microdata and JSON-LD) and both are handled, with the full description, ISO dates, employer metadata and structured salary.
4. The outbound apply redirect (`/direct/i`) is resolved to the employer's real ATS URL.
5. The merged row is pushed to the dataset — one row per job, JSON/CSV/Excel export.

### What makes this different from other Eluta actors

- **Apply links that actually apply.** Other actors return the eluta.ca job URL as `applyUrl`. This one resolves the redirect to the employer's ATS posting (e.g. `careersen-hrrh.icims.com/jobs/23804/...`).
- **Full description coverage.** Both eluta page variants are parsed, so descriptions come back complete — including postings where SERP-only scrapers return empty text.
- **Employer depth.** NAICS code, HQ address with coordinates, employer website/domain/logo/description, and Top-100 employer ratings — fields for lead-gen and market analysis, not just job listings.
- **Honest rate-limit handling.** Eluta soft-blocks bursty IPs; this actor detects it and rotates through a residential pool automatically instead of silently returning truncated results.
- **Real contact discovery, not description regex.** Other actors "extract emails" by pattern-matching the posting text — which contains no email for the vast majority of jobs. Opt-in `enrichEmails` discovers the hiring employer's actual contact emails from their website and public sources, and bills only per email actually found.

### Notes & limitations

- **Salary coverage follows the source.** Only a minority of Canadian employers publish pay on their postings (~20–30% in our tests). Where eluta has it, you get structured min/max; where it only appears in prose, the text parser picks it up best-effort.
- `applyUrl` falls back to the eluta job URL when the redirect can't be resolved (`applyUrlResolved: false`) — rare (≥85% resolve in testing).
- `topEmployerRating` only exists for employers recognised in Canada's Top 100 Employers project (eluta is run by its publisher, Mediacorp).
- Monitoring mode's first run returns everything matching — that seeds your baseline; subsequent runs return only new postings.
- Sponsored SERP slots are included and flagged via `isSponsored`.

### FAQ

**How fresh are the postings?**
Eluta indexes employer career sites continuously — with `sortBy: date` the top results are typically minutes to hours old, and each row carries `postedAgo` + an ISO `postedDate`.

**Can I scrape a specific employer's jobs?**
Yes — paste their eluta employer page (e.g. `https://www.eluta.ca/jobs-at-sunnybrook`) into `startUrls`.

**What does a "real apply URL" mean exactly?**
Eluta's apply buttons go through an internal redirect. The actor follows that redirect server-side and stores the destination — the employer's own ATS posting — in `applyUrl`, with the source career-site domain in `sourceDomain`.

**Does it cover all of Canada?**
Yes. Leave `location` empty to search nationally, or run multiple cities in one go — every query × location pair becomes its own search in the same run.

**How do I get only new jobs each day?**
Enable `monitoringMode`, set `sortBy: date`, and schedule the actor daily. Already-delivered job IDs are skipped without fetching (and without charge). `resetMonitoringState` rebuilds the baseline.

### Support

- **Bugs / feature requests** — open an issue on the GitHub repo
- **Custom exports / tailored fields** — drop a note via the Apify Store contact form
- **Other actors** — see my Apify Store profile for the rest of the catalog

### 🔎 Explore more scrapers

Same engine, other job boards:

- [TotalJobs Scraper](https://apify.com/memo23/totaljobs-scraper) — UK all sectors
- [IrishJobs.ie Scraper](https://apify.com/memo23/irishjobs-scraper) — Ireland's leading job board
- [StepStone Job Scraper](https://apify.com/memo23/stepstone-search-cheerio-ppr) — Germany, Austria, France, Denmark
- [s1jobs Scraper](https://apify.com/memo23/s1jobs-scraper) — Scotland
- [CWJobs Scraper](https://apify.com/memo23/cwjobs-scraper) — UK tech & IT

…plus 100+ more on the [memo23 profile](https://apify.com/memo23).

***

### ⚠️ Disclaimer

This Actor is an independent tool and is **not affiliated with, endorsed by, or sponsored by** Eluta.ca, Mediacorp Canada Inc., or any of their subsidiaries or affiliates. All trademarks mentioned are the property of their respective owners.

The scraper accesses only **publicly visible** job postings rendered server-side by eluta.ca — no login, no CAPTCHA solving, no authenticated endpoints. When eluta rate-limits an IP it backs off and rotates rather than attempting to bypass the verification page.

Users are responsible for:

- Complying with Eluta.ca's Terms of Service
- Following PIPEDA and your jurisdiction's data-protection laws (GDPR, CCPA, etc.) when storing, processing, or republishing scraped postings
- Not republishing scraped data in a way that competes commercially with Eluta.ca

***

### SEO Keywords

eluta scraper, scrape eluta.ca, eluta api, eluta.ca job scraper, Apify eluta, canada job scraper, canadian jobs api, canada job listings data, toronto jobs scraper, vancouver jobs scraper, canadian salary data, job postings canada api, canada hiring data, recruitment data canada, job market analytics canada, canada's top 100 employers data, naics job data, employer career site scraper, ats apply links, job feed canada, job monitoring alerts canada, indeed canada alternative, job bank canada alternative

# Actor input Schema

## `query` (type: `array`):

Job search keywords — each entry runs as its own eluta search. Eluta in-query qualifiers (e.g. `nurse sort:post`) are allowed.

## `location` (type: `array`):

City or region filters, e.g. `Toronto` or `Vancouver, BC`. Combined with every keyword. Leave empty to search all of Canada.

## `sortBy` (type: `string`):

Result ordering on the eluta side. `date` surfaces the newest postings first (recommended for monitoring), `relevance` is eluta's default ranking.

## `startUrls` (type: `array`):

Full eluta.ca URLs to crawl instead of (or in addition to) keyword searches. Supported: `/search?q=...`, SEO pages like `/Nurse-jobs-in-Toronto-ON`, employer pages like `/jobs-at-sunnybrook`, and direct job URLs (`/spl/...`).

## `includeDetails` (type: `boolean`):

Fetch each job's detail page for the full description (HTML + text), ISO posted/expiry dates, employment type, industry + NAICS code, employer website, logo, HQ address with latitude/longitude, and the Canada's-Top-100 employer rating. Disable for fast SERP-only rows.

## `resolveApplyUrls` (type: `boolean`):

Follow eluta's outbound redirect to capture the employer's real apply URL (e.g. `sunnybrook.talcura.com/...JobId=12800`) instead of just the eluta.ca job page. One extra lightweight request per job.

## `enrichEmails` (type: `boolean`):

If enabled, tries to find a contact email for each hiring employer. The employer website usually comes straight from the job page's own metadata, and its contact/about pages are scanned for an email. Adds contactEmail + contactWebsite columns plus a detailed emailEnrichment object. Best-effort, billed per contact email found; only charged when an email is returned, never for misses.

## `qualifyByPayment` (type: `boolean`):

Requires "Enrich with contact emails". Scans each employer's website — reusing the pages already fetched for email discovery, so no extra cost or time — for payment processors and e-commerce platforms (Stripe, Shopify, PayPal, Paddle, Lemon Squeezy, WooCommerce, Square, Chargebee and more). Adds takesPayments, paymentProcessors, stripeLiveKey and paymentConfidence. No extra charge — included with each enriched employer.

## `monitoringMode` (type: `boolean`):

When enabled, jobs whose eluta ID has already been delivered to you are silently skipped (no detail page is fetched and no charge applies for them). The first run after enabling monitoring returns every match — that builds your baseline. Every subsequent run returns only new postings. Combine with Apify's scheduler and `sortBy: date` for a 'jobs posted since last run' feed.

## `resetMonitoringState` (type: `boolean`):

One-shot toggle: clear the saved 'jobs already seen' record at the start of this run. Use this when you change search keywords, want to rebuild the baseline, or recover from a missed delivery. Has no effect when monitoring mode is off.

## `maxItems` (type: `integer`):

Hard cap on the number of jobs collected across all searches. Use this to limit billing.

## `maxPagesPerSearch` (type: `integer`):

Stop paginating each search after this many result pages (~10 jobs per page). The walk also stops automatically when a page returns no new jobs.

## `maxConcurrency` (type: `integer`):

Maximum number of job pages processed in parallel.

## `minConcurrency` (type: `integer`):

Minimum number of job pages processed in parallel.

## `maxRequestRetries` (type: `integer`):

Number of retries before a failed request is given up.

## Actor input object example

```json
{
  "query": [
    "software engineer"
  ],
  "location": [
    "Toronto"
  ],
  "sortBy": "relevance",
  "includeDetails": true,
  "resolveApplyUrls": true,
  "enrichEmails": false,
  "qualifyByPayment": false,
  "monitoringMode": false,
  "resetMonitoringState": false,
  "maxItems": 1000,
  "maxPagesPerSearch": 20,
  "maxConcurrency": 10,
  "minConcurrency": 1,
  "maxRequestRetries": 5
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "query": [
        "software engineer"
    ],
    "location": [
        "Toronto"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/eluta-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "query": ["software engineer"],
    "location": ["Toronto"],
}

# Run the Actor and wait for it to finish
run = client.actor("memo23/eluta-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "query": [
    "software engineer"
  ],
  "location": [
    "Toronto"
  ]
}' |
apify call memo23/eluta-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,memo23/eluta-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/P4ToaIg8BBnNIahYp/builds/cVOQyK48kQhJFpO0a/openapi.json
