# Salla Store Scraper — Products, Prices & Stock (`al_mansouri/salla-store-scraper`) Actor

Read any Salla (سلة) store and get every public product as clean structured data — Arabic names intact, prices in SAR, live availability. تصدير منتجات سلة.

- **URL**: https://apify.com/al\_mansouri/salla-store-scraper.md
- **Developed by:** [Hussein Al-Mansori](https://apify.com/al_mansouri) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

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

## Salla Store Scraper — Products, Prices & Stock (سلة)

Paste any **Salla** store address and get its public catalogue back as clean, structured data —
product names exactly as the store wrote them, prices in the store's own currency, and live
availability.

**متجر سلة → بيانات منتجات نظيفة.** Salla (سلة) powers tens of thousands of Saudi online stores.
This Actor reads them properly, in Arabic, without mangling a single character.

### Who this is for

| You are | What you get |
|---|---|
| **A merchant on Salla** | An export or backup of your own catalogue, in a file you can actually reopen |
| **A competing merchant or agency** | Competitor assortment and pricing across the Saudi market |
| **A market researcher** | Structured MENA e-commerce data, one row per product |

Exporting your own store is the case this was built for first. It is your catalogue, and Salla
gives you no easy button for it.

### What you paste in

A store home page, a category page, or a single product page — and you can mix them in one run.
Both kinds of Salla address work:

- a store on Salla's own domain, like `https://salla.sa/your-store`
- a store on its own domain, like `https://yourbrand.com`

If an address is not a Salla store, the result says exactly that instead of returning nothing.

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `startUrls` | list of URLs | — | The stores, categories, or products to read. Required. |
| `maxItems` | integer | `200` | Stop after this many products **from each store**. |
| `includeOutOfStock` | boolean | `true` | Keep sold-out products. Turn off for buyable stock only. |
| `includeVariants` | boolean | `true` | List options as variants when the store publishes them. |
| `language` | `auto` / `ar` / `en` | `auto` | Preferred storefront language when a store has more than one. |
| `respectRobots` | boolean | `true` | Skip any address the store's robots.txt asks crawlers not to read. Checked on every page read, not just the first. |
| `proxyConfiguration` | object | off | Leave off unless a store refuses the connection — see below. |

A run needs nothing but `startUrls`.

**On proxies, and on currency.** Two things worth knowing, both measured rather than assumed.

Salla sits behind a CDN that challenges data-centre addresses, so a data-centre proxy reaches
*fewer* stores than no proxy at all — the same store read cleanly on a direct connection and came
back as an unreadable challenge page through the data-centre proxy. Proxies are therefore off by
default, and a **residential** proxy is the escalation if a store refuses the connection.

Some stores also **localise by where the request comes from**. One store returned `290 SAR` read
from Saudi Arabia and `105 USD` read from a US data centre — a different currency *and* a
different number, for the same product. Every row carries its own `currency`, so nothing is
mislabelled, but if you need Saudi pricing specifically, run through a residential proxy pinned to
Saudi Arabia:

```json
{ "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "SA" }
```

### What you get back

![Real output from a live run: product rows with Arabic and English names, prices, sale prices,
stock, and SKUs, plus one summary row per store](https://api.apify.com/v2/key-value-stores/bex1Rd8jPAZnIq3Ye/records/output-preview.png)

Two kinds of row, told apart by `event`. Everything above is real output from a live run — no
sample data was written by hand.

**One `product-scraped` row per product:**

```json
{
  "event": "product-scraped",
  "platform": "salla",
  "store": { "name": "متجر النور", "domain": "salla.sa/alnoor", "id": "618430553" },
  "url": "https://salla.sa/alnoor/abaya/p12345",
  "nameAr": "عباية كاجوال قطن",
  "nameEn": null,
  "price": 149.0,
  "salePrice": 119.0,
  "currency": "SAR",
  "availability": "in_stock",
  "sku": "AB-1042",
  "brand": null,
  "categories": ["عبايات"],
  "images": ["https://cdn.salla.sa/....jpg"],
  "variants": [],
  "hasOptions": false,
  "scrapedAt": "2026-08-21T14:03:22Z",
  "status": "verified"
}
```

**One `store-analyzed` row per store**, summarising what happened:

```json
{
  "event": "store-analyzed",
  "platform": "salla",
  "store": { "name": "متجر النور", "domain": "salla.sa/alnoor", "id": "618430553" },
  "storeUrl": "https://salla.sa/alnoor",
  "productsDiscovered": 412,
  "productsScraped": 200,
  "productsWithOptions": 18,
  "pricesCrossChecked": 5,
  "pricesDisagreed": 0,
  "stoppedBy": "maxItems",
  "discoveryMethod": "storefront-api",
  "status": "warning",
  "warnings": [{ "code": "ITEM_LIMIT_REACHED", "message": "…" }]
}
```

Every row stands on its own — a product row repeats the store it came from, so one row is enough
to act on without joining it to anything.

#### Arabic, handled properly

Names come back in the script the store wrote them in. An Arabic name lands in `nameAr` and
`nameEn` stays `null`; an English name does the reverse. **Nothing is machine-translated**, and no
name is ever transliterated or stripped.

Downloading as CSV opens in Excel with Arabic intact — Apify prefixes CSV exports with a UTF-8
byte order mark, which is the thing Excel needs and the thing most exports forget.

#### How honest the row is

| `status` | Meaning |
|---|---|
| `verified` | Read cleanly. Nothing was uncertain. |
| `warning` | Delivered, but something was uncertain — the reason is on the row. |
| `failed` | Nothing could be delivered. Never charged. |

Three rules this Actor does not bend:

- **A field that could not be read is `null`.** It is never guessed, never zero, and never a
  placeholder. If a store publishes no SKU, `sku` is `null` — even when the page offers something
  SKU-shaped that is really just part of the address.
- **Stock is only ever what the store said.** A store that says nothing gets
  `availability: "unknown"`, not `"in_stock"`.
- **Some products are checked against the store's own product page.** A sample from each store is
  read twice, from the store's catalogue and from the product's own page. When the two disagree
  about price or stock, the row keeps the catalogue value and **stops calling itself verified** —
  it carries `PRICE_MISMATCH` or `AVAILABILITY_MISMATCH` so you know that number is contested. It
  happens on real stores: one live catalogue lists a perfume as in stock that its own product page
  marks sold out. The store row publishes `pricesCrossChecked` and `pricesDisagreed`, so you can
  see the size of the check and not just its verdict.

### Tested on 31 live Salla stores

Nobody else on the Store publishes numbers. These come from
`scripts/run_store_scorecard.py`, run on **2026-08-22 against 31 live Salla storefronts and 855
products** — sizes from 3 products to 600+, across perfume and oud, abayas, gifts, toys, dates and
coffee, electronics, sportswear, supplements, pet supplies, stationery and collectibles. Eleven
were read on the merchant's own domain and twenty on a `salla.sa` address.

Every figure below is a count from that run, not an estimate. Ask on the Actor's issues page if
you want the store list to reproduce it.

| Field | Extracted |
|---|---|
| Product name | **100%** |
| Price | **100%** |
| Availability | **100%** |
| Images | **99.4%** |
| Categories | **89.8%** |
| SKU | 13.7% |
| Variants | 0% |

**Cross-check: 153 products were also read from their own product page. 11 disagreed — 92.8%
agreement.** Those 11 are the reason the check exists; each is flagged on its row rather than
shipped as clean data.

Two of these numbers deserve their honest explanation rather than a flattering one:

- **SKU 13.7% is not a failure to extract.** Most Salla merchants never set a SKU. When they
  haven't, Salla's product page fills the field with the product's own URL slug — and this Actor
  refuses to publish that as a SKU. A scraper that passed the slug through would show a far
  prettier number and hand you a column you cannot match against your own catalogue.
- **Variants 0%, and 26% of products have them.** 220 of the 855 products told us they have
  options like colour or size. Salla does not publish the option *values* anywhere reachable
  without running the store's JavaScript, so this Actor does not invent them. Those products carry
  `hasOptions: true` so you know the variants exist and that we could not read them. Fixing this
  properly means adding a browser, which is a cost decision and not yet made.

### Error codes

| Code | Meaning | Retry? |
|---|---|---|
| `NOT_A_SALLA_STORE` | Reachable, but not a Salla store | No |
| `ROBOTS_DISALLOWED` | The store's robots.txt asks crawlers not to read it | No |
| `STORE_UNREACHABLE` | The store could not be reached | Yes |
| `STORE_TIMEOUT` | The store did not respond in time | Yes |
| `PRODUCT_PARSE_FAILED` | A product address you submitted publishes no readable details | No |
| `INVALID_URL` | The address is not a usable http/https URL | No |
| `BLOCKED_DESTINATION` | The address resolves somewhere requests are not sent | No |
| `DNS_FAILURE` | The hostname does not resolve | No |
| `STORAGE_FAILURE` | The result could not be saved. Never charged. | Yes |
| `BUDGET_EXHAUSTED` | The run's charge limit is spent | No |
| `INTERNAL_ERROR` | Something unexpected. Nothing internal is ever shown to you. | Yes |

A **closed** store is not an error. Salla stores can be switched off by their owner, and that is a
real answer: the row comes back with `stoppedBy: "storeClosed"` and a `STORE_CLOSED` warning,
naming the store, rather than pretending its catalogue is empty.

One unreadable product *inside* a catalogue is not an error either — it is left out, the rest are
still returned, and the store row carries a `PRODUCT_PARSE_FAILED` warning saying so.

`ROBOTS_DISALLOWED` above stops a whole store. When robots.txt closes only *some* addresses, the
run continues without them and the store row carries a `ROBOTS_LIMITED_DISCOVERY` warning saying
how many were skipped — so a catalogue shortened by the store's own instruction never looks
complete.

### Limits

| Limit | Value |
|---|---|
| Stores per run | 50 |
| Products per store | 5,000 (`maxItems`, default 200) |
| Time per store | 300 seconds |
| Stores read at once | 2 |
| Images kept per product | 20 |
| Variants kept per product | 50 |
| Products cross-checked per store | 5 |

### What this deliberately does not do

- No reviews, no customer data, no store-owner contact details. Product data only.
- No logged-in areas, carts, checkouts, or coupon probing. Public catalogue only.
- No image downloading — URLs only.
- No "search all of Salla": it reads the stores you name.

### Pricing

**Free.** Only Apify's platform usage applies. Two events are defined — `product-scraped` (one per
product) and `store-analyzed` (one per store per run) — and neither is charged today.

They exist so that adding a price later would be a price change and not a redesign. Nothing is
planned.

### Works well with

**Website Change Monitor with Smart Diff** — take a product address from this Actor and hand it to
the monitor to be told when that price moves.

**Zid Store Scraper** — the same job for Zid (زد), Salla's main rival. Identical field names and
identical values inside them, so the two datasets concatenate into one sheet with no renaming, and
every row's `platform` says which shop it came from.

***

### FAQ

**Does it work on a store with its own domain?**
Yes. Eleven of the 31 stores in the scorecard were read on the merchant's own domain.

**Does it need my store login?**
No. Public catalogue pages only. This Actor never touches logins, carts, checkout, or coupons.

**Does it get reviews or customer data?**
No. Product data only.

**Why is `sku` empty on so many rows?**
Because most Salla merchants never set one — 13.7% of the scorecard's products had a SKU at all.
That is the store's choice, not a failed read, and an invented value would be worse than `null`.

**Why are `variants` empty?**
Salla builds its option pickers in the browser, and this Actor reads the storefront API instead of
running one. Rather than guess, it reports nothing and says so. See the scorecard.

**Does `language: ar` translate the store?**
No. It says which language you expected; if the store does not publish it, the row carries a
`LANGUAGE_UNAVAILABLE` warning and you get the store's own language. Nothing is machine-translated.

**Does it download the images?**
No — you get URLs.

**Does it scrape Zid stores?**
No. That is the separate **Zid Store Scraper**.

**Can it find every Salla store for me?**
No. It reads the store URLs you give it.

***

### Support

Open an issue on the Actor's Apify page. Include the run ID and the store URL — the
`store-analyzed` row tells us most of what we need.

*Not affiliated with, endorsed by, or sponsored by Salla (سلة).*

# Actor input Schema

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

Salla store addresses, up to 50 per run. A store home page, a category page, or a single product page all work, and you can mix them. Both a store on salla.sa and a store on its own domain are supported.

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

Stop after this many products from each store. Raise it to take a whole catalogue; lower it for a quick sample.

## `includeOutOfStock` (type: `boolean`):

Keep products the store marks as sold out. Turn this off to return only what a shopper can buy right now.

## `includeVariants` (type: `boolean`):

List size, colour, and other options as separate variants when the store publishes them. Many Salla stores publish none, and the result says so rather than inventing any.

## `language` (type: `string`):

Which storefront language to prefer when a store publishes more than one. Product names are returned in the script the store actually wrote them in, and are never machine-translated.

## `respectRobots` (type: `boolean`):

Skip any address the store asks crawlers not to read. Turning this off is your decision and is recorded in the result.

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

Proxy used for requests. Off by default: Salla sits behind a CDN that challenges data-centre addresses, and the platform's own connection reaches stores more reliably than its data-centre proxy does. Turn on a residential proxy for stores that refuse it outright.

## Actor input object example

```json
{
  "startUrls": [
    "https://salla.sa/example-store"
  ],
  "maxItems": 200,
  "includeOutOfStock": true,
  "includeVariants": true,
  "language": "auto",
  "respectRobots": true
}
```

# Actor output Schema

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

One machine-readable result per store, including stores that could not be read.

## `summary` (type: `string`):

No description

# 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 = {
    "startUrls": [
        "https://salla.sa/karz11"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("al_mansouri/salla-store-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 = { "startUrls": ["https://salla.sa/karz11"] }

# Run the Actor and wait for it to finish
run = client.actor("al_mansouri/salla-store-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 '{
  "startUrls": [
    "https://salla.sa/karz11"
  ]
}' |
apify call al_mansouri/salla-store-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,al_mansouri/salla-store-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/Gioz5mRqGtkEOvhdq/builds/cDCDz03CrSRO8ZbPb/openapi.json
