# Kaspi.kz Products Scraper (`scrapyx/kaspi-products-scraper`) Actor

Products from Kaspi.kz, Kazakhstan's largest marketplace: price, bonus price, installments, merchants, rating and per-city delivery. The city is validated against Kaspi's own list of 320 first -- an unknown one is answered by Almaty, not by an error.

- **URL**: https://apify.com/scrapyx/kaspi-products-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.75 / 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.
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

## Kaspi.kz Products Scraper

Products from **Kaspi.kz**, Kazakhstan's dominant marketplace: price, sale
price, the loyalty-bonus price, monthly installment terms, the merchants
selling it, rating and review count, and the delivery window for the city you
ask about.

Built on the storefront's own JSON search. HTTP only, no browser, no key.

### What it is for

- **Price monitoring** in a market Amazon and AliExpress do not cover.
- **Assortment research** — what is listed, by whom, at what price.
- **Merchant intelligence** — every row carries the sellers and how many there are.
- **Credit-terms analysis** — Kaspi sells on installments, and the monthly
  figure is a first-class field.

### Input

| field | what it does |
| --- | --- |
| `searchTerms` | What to search for — `iphone 15`, `ноутбук`, `холодильник`. |
| `city` | Slug, Russian name, English name or numeric id. Default: Almaty. |
| `onlyDiscounted` | Keep only products whose sale price is below list. |
| `includePromoted` | Add sponsored placements, flagged with `isPromoted`. |
| `maxItems`, `maxConcurrency`, `minRequestInterval` | Limits and pacing. |

### Four things about this data worth knowing

#### 1. Three ways to ask for a city are accepted. One works.

`X-KS-City` is the header Kaspi's own front end sends and the one every
write-up reaches for. On this endpoint it does **nothing**. Measured on one
query, reading back the delivery windows:

| how the city was sent | delivery windows returned |
| --- | --- |
| not sent at all | `EXPRESS` |
| header `X-KS-City: 750000000` | `EXPRESS` — **no change** |
| `?cityId=710000000` | `EXPRESS` — **ignored** |
| `?c=999999999` (a city that does not exist) | `EXPRESS` — **falls back to Almaty** |
| `?c=710000000` (Astana) | `EXPRESS, TODAY, TOMORROW` ✓ |
| `?c=511010000` (Shymkent) | `EXPRESS, TILL_2_DAYS, TILL_5_DAYS, TOMORROW` ✓ |

All six return HTTP 200 with twelve real products. Nothing in the response says
which city served it.

So this Actor sends `c=`, and **checks your city against Kaspi's own list of 320
first** — an unknown one stops the run rather than quietly returning Almaty.

#### 2. The capital has three names and the one you would type is neither

Kaspi's own record for Astana is:

```
id 710000000   slug "nur-sultan"   name "Астана"   English "Astana"
```

The slug is still the city's **former** name, the `name` field is Cyrillic, and
`astana` matches neither. All four spellings resolve here.

#### 3. The city always changes delivery. It changes some prices too.

The first measurement for this Actor compared the top 12 results across four
cities, found every price identical, and concluded prices were national. A
wider sample disproved it:

| query "iphone 15", 48 products per city, vs Almaty | prices differing |
| --- | --- |
| Astana | 5 / 48 (10.4%) |
| Shymkent | 4 / 48 (8.3%) |
| Atyrau | 3 / 48 (6.2%) |

Most gaps are trivial — 538,833 vs 538,868 ₸ — but not all: 567,309 vs 543,138 ₸
is a 24,000 ₸ difference on the same product id.

So the Actor makes **no claim** about this in its output. It reports the
delivery windows it actually saw, which is the evidence your city took effect,
and leaves the prices to speak for themselves.

#### 4. There is no result count, and an empty search is a firehose

The response body is exactly `{"data": [...], "promotedCards": …,
"promotedItems": …}`. There is no total in the body and none in the headers, so
nothing here is ever phrased as "N of M" — `matchCountAvailable: false` says so
outright.

And the two ways a search can go wrong are asymmetric:

```
text=zzqqxxnotarealproduct  ->  200, 0 products      honest
text=                       ->  200, 12 products     a generic catalogue
no text parameter at all    ->  200, 12 products     the same generic page
```

The dangerous one is the second: it returns a page that looks like a working
run. An empty term is refused before it is sent.

### Output

- **`PRODUCT`** — `title`, `brand`, `price`, `salePrice`, `priceMinusBonus`
  (the loyalty price, kept separate from the shelf price), `creditMonthlyPrice`,
  `installmentMonths`, `rating`, `reviewsQuantity`, `merchants[]`,
  `deliveryDuration`, `images[]`, `productUrl`, `isPromoted`.
- **`SEARCH_SUMMARY`** — `productsReturned`, `matchCountAvailable: false`,
  `deliveryWindowsSeen`, `keywordMatchShare`, `duplicateRowsDropped`,
  `ceilingHit`, `stoppedReason`.
- **`ERROR`** — one row naming what went wrong, instead of a silent empty.

### Technical notes

- **The proxy is ON by default, and it needs to be.** Kaspi does not challenge
  a plain residential IP at all — five TLS profiles were clean cold. It *does*
  answer **HTTP 429** to the address an Apify run egresses from directly: a
  cloud run with no proxy failed all four attempts on every request, while the
  identical code from a residential IP succeeded first time. Apify's **free
  datacenter** proxy clears it completely, so the default costs nothing. This
  is a property of Apify's egress, not of datacenter IPs in general — no need
  to reach for residential and its per-gigabyte bill.
- **The city list has a built-in fallback.** The storefront HTML is rate-limited
  harder than the JSON, so even through the proxy it sometimes 429s. The city
  guard matters more than its freshness, so a failed live read degrades to a
  320-city snapshot taken 2026-09-16 and the summary says which was used
  (`cityListSource`). A city added since then would be refused.
- **The keyword check reads the category, not just the title.** On Kaspi a
  Russian search returns Latin-named products — `ноутбук` gives
  `Industria NT107 15.6" / 16 Гб / SSD 512 Гб`. The Russian word is in the
  category path (`Ноутбуки`), so `keywordMatchShare` matches across title,
  brand and category together. A title-only check scored a perfectly good
  48-row search 0.0.
- **HTTP-only**, `curl_cffi` `chrome124`. No WAF, no cookie, no key. The
  endpoint needs a `Referer` on the kaspi.kz origin; nothing else.
- **Paging stops at page 300.** Page 301 is an HTTP 400 whose body names
  nothing (`{"status":400,"error":"Bad Request","path":"/pl/results"}`), so the
  wall is checked before the request rather than discovered by hitting it. At
  12 products per page that is about 3,600 per search term.
- **Adjacent pages overlap** — Kaspi re-ranks live, and pages 0 and 1 shared 2
  of 12 ids in one measurement. Ids are deduplicated and the collisions counted
  in `duplicateRowsDropped`, so row counts are item counts.
- **Sponsored placements arrive in their own response keys** rather than mixed
  into the results, which is unusually honest of Kaspi. They are still excluded
  by default and flagged when included.
- **robots.txt checked** on 2026-09-16: no AI-bot group, no blanket disallow,
  and both paths this Actor reads are allowed.

### Known limits

- Kazakhstan only, prices in tenge.
- No match count, by construction — see above.
- \~3,600 products per search term. Slice by narrower terms to go wider.
- Product **detail** pages are not read; everything here comes from the search
  result card, which is already rich.

# Actor input Schema

## `searchTerms` (type: `array`):

What to search for on Kaspi - 'Ð½Ð¾ÑƒÑ‚Ð±ÑƒÐº', 'iphone 15', 'Ñ…Ð¾Ð»Ð¾Ð´Ð¸Ð»ÑŒÐ½Ð¸Ðº'. Each term becomes its own target with its own summary row. An empty term is refused here rather than sent: Kaspi answers one with a generic twelve-product catalogue instead of an error, which is the failure mode that looks like a working run.

## `city` (type: `string`):

A Kaspi city slug, Russian name or numeric id - almaty, astana, shymkent, 750000000. Leave empty for Almaty, Kaspi's own default. IMPORTANT: Kaspi does NOT reject an unknown city id; it answers with the default city and twelve real products. Every value is therefore checked against Kaspi's own list of 320 cities first. Note also that the city changes DELIVERY WINDOWS only - prices on this endpoint are national, and every run reports the delivery windows it actually saw as evidence the city took effect.

## `onlyDiscounted` (type: `boolean`):

Keep only products whose sale price is below their list price.

## `includePromoted` (type: `boolean`):

Kaspi returns sponsored placements in their own keys rather than mixed into the results, which is unusually honest. They are excluded by default; enabling this adds them, flagged with isPromoted.

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

Overall cap on product rows across every search term. Kaspi serves 12 products per page and refuses page 301 with a message-less HTTP 400, so a single search term tops out at about 3,600 products however deep you page. A run that reaches that wall reports ceilingHit.

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

Parallel requests across search terms.

## `minRequestInterval` (type: `integer`):

Politeness delay between request starts. It paces starts only and does not hold a concurrency slot, so raising it slows the run without idling workers.

## `proxyConfiguration` (type: `object`):

ON by default, and it needs to be. Kaspi answers HTTP 429 to the IP an Apify run egresses from directly - a cloud run without a proxy fails every request, while the same code from a residential IP succeeds on the first attempt. Apify's FREE datacenter proxy clears it completely (verified: 429 without, 200 with), so this costs nothing. Leave the pool unpinned: the city travels in the query string, not in the exit IP.

## Actor input object example

```json
{
  "searchTerms": [
    "iphone 15"
  ],
  "onlyDiscounted": false,
  "includePromoted": false,
  "maxItems": 240,
  "maxConcurrency": 2,
  "minRequestInterval": 0,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

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

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/kaspi-products-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 = { "searchTerms": ["iphone 15"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/kaspi-products-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 '{
  "searchTerms": [
    "iphone 15"
  ]
}' |
apify call scrapyx/kaspi-products-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/kaspi-products-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/jqRvVUR5v1Zx0Jqhw/builds/rV4pChuasdx3Fk4On/openapi.json
