# Zid Scraper - Saudi Store Product & Price Tracker (SAR) (`superslowsloth/zid-store-products`) Actor

Zid scraper: <store>.zid.store addresses to one row per product with SAR price, sale price, discount amount and percent, on-sale flag, SKU, categories and product link. For Saudi competitor catalogue and price watching. Residential proxy required.

- **URL**: https://apify.com/superslowsloth/zid-store-products.md
- **Developed by:** [Superslow Sloth](https://apify.com/superslowsloth) (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 $1.75 / 1,000 product scrapeds

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

## Zid scraper: Saudi store product and price tracker (SAR)

This Zid scraper reads the whole product catalogue of any [Zid](https://zid.sa)
store (the Saudi store builder; stores live at `<name>.zid.store`) and returns
one flat row per product: the price in **SAR**, the sale price, the discount in
riyals and percent, an on-sale flag, SKU, categories and the product link. Use
it as a **Saudi competitor catalogue and price watch**: give it a list of
competitor stores, run it on a schedule and diff the dataset to see which
products were added, repriced or put on sale since last week.

No account, no API key, no login, no browser. One request returns 24 products.
**It needs the Apify RESIDENTIAL proxy** (see below): the datacenter proxy is
refused.

### Input

| Field | Notes |
|---|---|
| `storeUrls` | One or more Zid stores, one per line. Any of: the link (`https://oav.zid.store`, a link to any page of the store works, only the host is used), the address (`alamirah-sa.zid.store`) or just the store name (`oav`). Each store's whole catalogue is read. A store that does not exist fails at no charge. |
| `maxItems` | Products to read **per store** (default 100, up to 10,000). Zid lists 24 per page. |
| `proxyConfiguration` | Apify RESIDENTIAL proxy by default. Leave it. |

Only stores at `<name>.zid.store` are supported. A store on a **custom domain**
(one a merchant has moved to its own domain, `shop.example.com`) is **refused
with an input error before the run starts and before anything is charged**: no
Zid store on a custom domain could be found to measure, so the actor does not
claim to read them. Use the store's
`<name>.zid.store` address instead (it normally keeps working next to a custom
domain; the actor cannot check that for you).

Example input:

```json
{
  "storeUrls": ["https://oav.zid.store", "alamirah-sa.zid.store"],
  "maxItems": 300
}
```

### Output

Prices are numbers in the store's currency; `currency` is `SAR` on every row
measured.

| Field | Notes |
|---|---|
| `store` | The store's host (`oav.zid.store`), so rows from several stores in one dataset can be told apart. |
| `id` | Zid's product id (a UUID string), unique within a store. |
| `parent_id` | The parent product's id when the entry is a variant. Null on all 326 products measured; the catalogue lists parent products only. |
| `name` | The product name as the merchant wrote it (Arabic, English, both, or a bare number), surrounding spaces trimmed. |
| `sku` | The merchant's stock-keeping code. Null if absent. |
| `currency` | `SAR` on every row measured; passed through as the store sends it. |
| `price` | The regular price. When the product is discounted this is the price **before** the discount. Null if the page sends none. |
| `sale_price` | The discounted price. **Null when the product is not discounted**, which is most of them (279 of 326 on the two stores measured). |
| `on_sale` | True when `sale_price` is below `price`; False when there is no lower sale price; null only if `price` is missing. |
| `discount_amount`, `discount_percent` | `price` minus `sale_price` in riyals, and the same as a percent of `price` to two decimals (490 to 295 is `195.0` and `39.8`). **Null unless both prices are present and the sale price is lower**, never 0. |
| `categories` | The names of the categories the product is filed in, as the merchant wrote them (often bilingual: `العبايات \| abayas`). Empty list if none (44 of 326 products). |
| `url` | The product page link, taken from the store page's own list. Null if the page did not carry one that lines up with the products. |

Example row (real values, 2026-10-19):

```json
{
  "store": "oav.zid.store",
  "id": "d4f75c78-d15c-451a-addf-a21b7bcf2219",
  "parent_id": null,
  "name": "29",
  "sku": "Z.1764700864734276",
  "currency": "SAR",
  "price": 770.0,
  "sale_price": null,
  "on_sale": false,
  "discount_amount": null,
  "discount_percent": null,
  "categories": [],
  "url": "https://oav.zid.store/products/29"
}
```

#### Honest nulls

A field the page did not send is `null`, never `0`. Two of the 326 products
measured were sent with a price of `0.0` (one per store, probably unpriced
items); they are returned as sent, because the store said it, and are not
turned into nulls or dropped.

- **No stock, images or descriptions.** The catalogue list does not carry them;
  this is a price and catalogue watch, not a full product export.
- **No variants as rows.** Only parent products are listed (`parent_id` was null
  on every row).
- **Prices as the store lists them.** The actor does not say whether a store
  shows prices with or without VAT; Zid does not say in the list.

### Pricing

Pay per event:

- **$0.002** per run (`actor-start`), charged only after your input is
  validated, so a run that fails on bad input costs nothing.
- **$0.00175** per product row written (`product-scraped`), i.e. $1.75 per
  1,000 products. Products that are not written are not charged.

### Proxy: residential only

Zid stores sit behind Cloudflare. When this actor was built, every request from
an Apify **datacenter** address was refused with HTTP 403 and Cloudflare's "Just
a moment" page, while the **residential** group read every page (8 of 8, then 6
of 6 requests). The default input therefore selects `RESIDENTIAL`. If you switch
to the datacenter proxy, or no proxy, expect every page to fail; the run logs a
warning and fails with the refusal message rather than returning empty rows.

### How it fetches

It reads the product list a Zid store page embeds for its own analytics (the
`all_products` argument of `zidTracking.__internal.sendGaProductsViewItemsList`)
instead of the theme's HTML cards, so it does not depend on the store's theme:
the same eight fields were found on two stores with different themes
(`oav.zid.store`, `alamirah-sa.zid.store`; 14 pages, 326 products). The product
link comes from the page's schema.org product list, matched to the products by
place and name.

Things worth knowing, all measured on 2026-10-19:

- **24 products per page; a page past the end is HTTP 404.** Reading stops
  there. That is the end of the catalogue, not an error. A store that does not
  exist is also a 404, but with a different body, and fails that store.
- **Pages do not overlap**, and a product seen twice in one walk is written and
  charged once.
- **A refusal is retried from a fresh address.** HTTP 403, 429, 5xx, a
  Cloudflare "Just a moment" page (even with a 200) or a dropped connection
  rotates the proxy exit and retries. A store whose pages keep being refused
  fails with the reason, and what was read before stays in the dataset.
- **A search or page with no product list** reads as a store with no products
  and costs nothing.
- **Depth.** At most 10,000 products per store are read (417 pages).

If Zid changes its page data, the run fails with a message or returns fewer
fields as nulls instead of inventing values.

# Actor input Schema

## `storeUrls` (type: `array`):

The Zid stores to read, one per line: the store's link (https://oav.zid.store), its address (alamirah-sa.zid.store) or just its name (oav). Only <name>.zid.store stores are supported; a store on its own custom domain is refused with an error, because it was not measured. Each store's whole catalogue is read, 24 products per page. A store that does not exist fails at no charge.

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

Read at most this many products from each store. Zid lists 24 per page, so 100 reads five pages. Up to 10,000 are accepted.

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

Use the Apify RESIDENTIAL proxy group (the default). Zid stores sit behind Cloudflare, which refused every Apify datacenter address when this actor was built (HTTP 403, 'Just a moment'), so the datacenter proxy does not work. A refused request is retried from a fresh address.

## Actor input object example

```json
{
  "storeUrls": [
    "https://oav.zid.store"
  ],
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `items` (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 = {
    "storeUrls": [
        "https://oav.zid.store"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/zid-store-products").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 = {
    "storeUrls": ["https://oav.zid.store"],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("superslowsloth/zid-store-products").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 '{
  "storeUrls": [
    "https://oav.zid.store"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call superslowsloth/zid-store-products --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,superslowsloth/zid-store-products"
        }
    }
}
```

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/c6ElrvkzhPfZlbged/builds/UX4dX9nLhkktoQLa4/openapi.json
