# Kijiji Listings API & Scraper - Monitor New Ads & Price Changes (`neverempty/kijiji-listings-monitor`) Actor

For resellers, deal alerts and price trackers: Kijiji.ca Canada classifieds as an API, by search term, category and location, with price, place, posted time, photos and attributes. Watch mode returns only ads posted since your last check and price changes, never old ads bumped back to the top.

- **URL**: https://apify.com/neverempty/kijiji-listings-monitor.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.63 / 1,000 listing returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Kijiji Listings API & Scraper - Monitor New Ads & Price Changes

Get listings from **Kijiji.ca**, Canada's classifieds site, by search term, category and location. One row per listing: title, price in CAD, place, posted time, photos, category, attributes and link. **Watch mode** turns a search into a feed: run it on a schedule and each run returns only the ads posted since your watch started that you have not received yet, plus ads whose price changed.

No login, no cookies, no API key. It reads the public search result pages only.

### What you can do with it

- **Deal alerts**: watch "iphone 15" in Cell Phones in Toronto every 15 minutes and get only the new ads.
- **Reselling and sourcing**: list a category in a city, filter by price range, and send the rows to a sheet or a bot.
- **Price tracking**: watch mode reports a `price-change` row with the previous price when a seller edits the price.
- **Market research**: how many listings Kijiji shows for a search (`listingsInSearch`), prices, and seller attributes.

### Input

| Field | What it does |
|---|---|
| `searchQueries` | Search terms, one per line. Leave empty to list a whole category or location. |
| `categoryIds` | Kijiji's category numbers: the digits after `c` at the end of a Kijiji search address. In `.../iphone/k0c760l1700273` the category is `760` (Cell Phones). |
| `locationIds` | Kijiji's location numbers: the digits after `l`. `1700273` is City of Toronto, `0` (or empty) is all of Canada. |
| `searchUrls` | Or paste a Kijiji search address, without the part after `?`. |
| `maxResultsPerSearch` | 1 to 1000, default 40 (one page). |
| `minPrice`, `maxPrice` | Price range in CAD, applied to the pages read. |
| `includeTopAds` | Also return the paid Top Ads Kijiji puts above each page. Off by default. |
| `onlyNew` | Watch mode. |
| `includePriceChanges` | Watch mode: also return price changes. On by default. |
| `watchPages` | Watch mode: most pages read per check, 1 to 10, default 3. |
| `watchName` | Optional name, to keep two watches of the same search apart. |
| `resetMonitoringState` | Start the watch over. |

Every term is combined with every category and every location, up to 10 searches per run.

Example: new iPhone ads in Cell Phones, City of Toronto.

```json
{
  "searchQueries": ["iphone 15"],
  "categoryIds": ["760"],
  "locationIds": ["1700273"],
  "onlyNew": true
}
```

### Output

```json
{
  "status": "ok",
  "changeType": "new",
  "listingId": "1744224216",
  "title": "Black IPhone 15",
  "price": 650,
  "priceType": "FIXED",
  "currency": "CAD",
  "previousPrice": null,
  "priceChange": null,
  "description": "…",
  "locationName": "City of Toronto",
  "address": "…",
  "categoryId": 760,
  "postedAt": "2026-10-01T15:44:19.000Z",
  "sortedAt": "2026-10-01T15:44:19.000Z",
  "imageUrl": "https://media.kijiji.ca/…",
  "imageUrls": ["https://media.kijiji.ca/…"],
  "attributes": { "forsaleby": ["ownr"], "condition": ["usedgood"] },
  "isTopAd": false,
  "url": "https://www.kijiji.ca/v-cell-phone/city-of-toronto/black-iphone-15/1744224216",
  "positionInSearch": 3,
  "sortOrder": "best-match",
  "listingsInSearch": 194,
  "searchCategoryName": "Cell Phones",
  "searchLocationName": "City of Toronto"
}
```

- `price` is a number of Canadian dollars, or `null` when the ad has no amount. `priceType` is Kijiji's own word (`FIXED`, `CONTACT`, `GIVE_AWAY`, ...), passed through as is.
- `postedAt` is when the ad was first posted. `sortedAt` is the time Kijiji sorts it by; it is later than `postedAt` when an old ad was bumped back up.
- `attributes` are Kijiji's own attribute names and codes for the category (for example `forsaleby`, `condition`), passed through as is.
- `description` is the snippet shown in the list, not the full text. Phone numbers and email addresses written into titles and descriptions are replaced with `[phone removed]` and `[email removed]`.

### Watch mode, and what "new" means here

The first run returns the current first page (rows marked `first-check`) and remembers it. Each later run returns:

- `new`: an ad you have not received whose **posted time is after your watch started**. An old ad that was bumped to the top, or that surfaced from a deeper page, is not called new.
- `price-change`: an ad you already saw whose price is different now, with `previousPrice` and `priceChange` (negative for a drop).

A row is remembered only after it was delivered. If a run hits its maximum charge or `maxResultsPerSearch`, the rest come back on the next run. If a page could not be read, the watch judges nothing for that search and says so.

When a check finds nothing, you get one free row with `status: "no-new-listings"` and the check is charged as one `search-checked` event instead of rows.

### The sort order is Kijiji's, and it matters

Kijiji's robots.txt disallows the sort, price, radius and other filter parameters, so this Actor never requests them and reads each search in the order Kijiji serves it. Measured on 2026-10-05:

- A search **without a search term** (a category and/or a location) is served **newest first**. A watch on it stops reading as soon as it reaches ads older than the last check. This is the most complete way to watch.
- A search **with a search term** is served sometimes newest first and sometimes in **best-match** order, even for the same address. In best-match order a new ad can sit on any page, so a watch reads `watchPages` pages and can miss new ads beyond them. The `no-new-listings` row says so when it happened.

Every row has `sortOrder` (`date` or `best-match`) so you can see which one you got. If you need every new ad for a keyword, watch the category and location without a search term and filter the titles on your side.

### Rows that explain instead of data (never charged)

`no-results`, `no-new-listings`, `none-in-price-range`, `not-found` (an unknown category or location number: Kijiji answers those with an empty page, and this Actor does not pass that off as "no listings"), `not-allowed-by-robots-txt`, `incomplete`, `unreadable`, `blocked`, `budget-reached`, `more-not-returned`, `more-new-than-readable`, `bad-input`.

If Kijiji shows a check page (CAPTCHA), the Actor stops and tells you. It does not solve or bypass check pages.

### Limits

- Up to 25 pages (1,000 listings) per search and 10 searches per run.
- Fields come from the search result pages. The full description, the seller's name and phone number are not read.
- Two overlapping runs of the same watch can, rarely, deliver the same ad twice. Do not schedule the same search and `watchName` to overlap.
- Not affiliated with Kijiji. Use the data in line with Kijiji's terms and the law that applies to you.

### Thanks for using this Actor

We build these tools for people who run them every day, and we improve them from what users tell us.

- **Missing a field, or need another filter?** Tell us in the **Issues** tab. If the data is there, we add it.
- **Found a bug or a wrong value?** Post the run ID in the **Issues** tab. Wrong data is the thing we fix first.

If this Actor saved you time, a short review helps other people find it.

# Actor input Schema

## `searchQueries` (type: `array`):

What to search for on Kijiji.ca, one term per line (for example iphone 15, road bike, sectional sofa). Each term is searched in every category and location below. Leave empty to list a whole category or location, which Kijiji serves newest first.

## `categoryIds` (type: `array`):

Kijiji's own category numbers: the digits after the letter c at the end of a Kijiji search address. Example: in kijiji.ca/b-cell-phone/city-of-toronto/iphone/k0c760l1700273 the category is 760 (Cell Phones). Leave empty for all categories.

## `locationIds` (type: `array`):

Kijiji's own location numbers: the digits after the letter l at the end of a Kijiji search address. Example: 1700273 is City of Toronto, 0 is all of Canada. Leave empty for all of Canada.

## `searchUrls` (type: `array`):

Or paste the address of a Kijiji search results page, without the part after the question mark, for example https://www.kijiji.ca/b-cell-phone/city-of-toronto/iphone/k0c760l1700273. Filter, sort and radius parameters are not allowed by Kijiji's robots.txt and are never requested.

## `maxResultsPerSearch` (type: `integer`):

How many listings to return for each search, from 1 to 1000. Kijiji shows 40 per page.

## `minPrice` (type: `integer`):

Return only listings priced at or above this many Canadian dollars. Applied to the pages read. Listings without a price (Please Contact, Free, Swap) are left out when a price range is set.

## `maxPrice` (type: `integer`):

Return only listings priced at or below this many Canadian dollars. Applied to the pages read.

## `includeTopAds` (type: `boolean`):

Kijiji puts paid Top Ads above each page of results; they are often months old. Off by default, so you get the organic list only. A listing never comes back twice.

## `onlyNew` (type: `boolean`):

Run the same search on a schedule. The first run returns the current first page and remembers it. Every later run returns only listings posted since the watch started that you have not received yet, and listings whose price changed. A check that finds nothing is charged as one small search-checked event instead of rows.

## `includePriceChanges` (type: `boolean`):

In watch mode, also return listings you already received when their price went up or down, with the previous price.

## `watchPages` (type: `integer`):

The most pages (40 listings each) a watch reads per search on each check, from 1 to 10. When Kijiji serves the list newest first, the watch stops as soon as it reaches listings older than the last check.

## `watchName` (type: `string`):

Optional. A name for this watch (letters, digits, dot, dash, underscore). Two schedules with the same search but different names keep separate memories.

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

Start this watch over: the run is treated as the first check again.

## Actor input object example

```json
{
  "searchQueries": [
    "iphone"
  ],
  "maxResultsPerSearch": 40,
  "includeTopAds": false,
  "onlyNew": false,
  "includePriceChanges": true,
  "watchPages": 3,
  "resetMonitoringState": false
}
```

# Actor output Schema

## `results` (type: `string`):

One row per Kijiji listing: listing ID, title, price in CAD with Kijiji's price type, description snippet, place and address shown, posted time, photos, category, attributes, Top Ad flag, link, its position and the sort order Kijiji served. Watch mode marks rows first-check, new or price-change (with the previous price). A search with no listings, nothing new, an unknown category or location number, a page robots.txt does not allow, a refused request or a run that hit its maximum charge comes back as a free row that says why.

# 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 = {
    "searchQueries": [
        "iphone"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/kijiji-listings-monitor").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 = { "searchQueries": ["iphone"] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/kijiji-listings-monitor").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 '{
  "searchQueries": [
    "iphone"
  ]
}' |
apify call neverempty/kijiji-listings-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/kijiji-listings-monitor"
        }
    }
}
```

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/LNnAHACgcYluG2Swq/builds/h1WSb2UBpmPSoaA8G/openapi.json
