Pick n Pay Scraper - South African Grocery Prices & Promotions
Pricing
from $1.05 / 1,000 product scrapeds
Pick n Pay Scraper - South African Grocery Prices & Promotions
Pick n Pay scraper: pnp.co.za product search by term or category to one row per product with price in ZAR, was-price and savings, Smart Shopper promotions, badges, stock, rating, unit price, category path and image. For South African grocery price and promotion watching.
Pricing
from $1.05 / 1,000 product scrapeds
Rating
0.0
(0)
Developer
Superslow Sloth
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Security
Limited permissionsShare
Pick n Pay scraper: South African grocery prices and promotions
This Pick n Pay scraper reads the product search of pnp.co.za
and returns one flat row per product: the shelf price in rand (ZAR), the
was-price and saving when the price is marked down, the Smart Shopper offers
attached to the product (Combo For R49.99, 3 For R65.00) with their end date,
the tile badges, whether it is in stock, rating and review count, the price per
kg for products sold by weight, the category path, the supplier, the product link
and the photo. Search by term, by category, or both, for any Pick n Pay
store. Run it on a schedule and diff the dataset to get a South African grocery
price and promotion watch: which milk, bread or braai-pack went on promotion,
what it cost in the Eastern Cape against the Western Cape, what dropped.
No account, no API key, no login, no browser. One request returns up to 100 products.
Input
| Field | Notes |
|---|---|
searchTerms | What to search for, one per line (milk, coca cola, basmati rice). Each term is a search of its own, and its rows carry it in search_term. Leave empty to read whole categories. |
categories | Restrict the search to categories, or list whole categories when there are no search terms. One per line: a top-level name (bakery, milk-dairy-and-eggs, liquor-store), a category code from a pnp.co.za address (milk-dairy-and-eggs-423144840, or a sub-category such as cheese44805192), or the address itself (https://www.pnp.co.za/c/bakery-423144840). With search terms, every term is searched inside every category. |
onPromotionOnly | Keep only products Pick n Pay lists under its On Promotion filter. Default off. |
sort | relevance (best match, the default), price-asc or price-desc. It is also the order rows are written in. |
storeCode | The Pick n Pay store whose prices are read, WC44 by default. Prices differ by store (see below). A code Pick n Pay does not know fails the run with an error. |
maxItems | Products written in total, across all searches (default 100, up to 10,000). |
maxItemsPerSearch | Optional limit for each search term or category, so one broad term cannot use the whole maxItems allowance. |
proxyConfiguration | Apify proxy with automatic datacenter exits by default. |
Example input:
{"searchTerms": ["milk", "coca cola"],"categories": ["milk-dairy-and-eggs"],"onPromotionOnly": true,"sort": "price-asc","storeCode": "WC44","maxItems": 100,"maxItemsPerSearch": 50}
The top-level categories on 2026-11-05: food-cupboard, personal-care-and-hygiene,
liquor-store, chocolates-chips-and-snacks, outdoor-and-diy, beverages,
household-and-cleaning, home-and-appliances, health-and-wellness, stationery,
pet-care, milk-dairy-and-eggs, frozen-food, baby, coffee-tea-and-hot-drinks,
electronics-and-office, toys-and-games, fresh-fruit-and-vegetables, bakery,
deli-and-party, fresh-meat-poultry-and-seafood, ready-meals-and-desserts,
flowers-and-plants, office-equipment.
Output
Prices are numbers in South African rand; currency is ZAR. One row is one
selling unit: a six-pack case of milk and a single bottle of the same milk are
two products with two codes. The same code is unique within one search.
| Field | Notes |
|---|---|
search_term, category, sort, position, store_code | What the row was found by: the term (null for a category listing), the category code the search was restricted to (null for none), the sort order, the place in that listing counting from 1, and the store whose prices these are. Relevance order drifts a little between requests. |
code, base_code, unit_code | Pick n Pay's id of the selling unit (000000000000214534_EA), the part before the underscore (the same for every selling unit of one product), and the part after it: seen as EA (a single), CS (a case), CK (a pack) and KGM (sold by the kilogram). |
name, url, image_url | The name as Pick n Pay prints it, the product page, and the 400 x 400 photo (null when there is none). |
supplier | The supplier Pick n Pay lists the product under (TIGER BRANDS, PICK N PAY for its own range). The seller of record, not always the brand on the label. |
category_path | The category segments of the product address, top level first (["Bakery", "Cakes & Treats", "Cakes"]). |
currency | ZAR. |
price | The shelf price for the selling unit, already net of a promotion applied to the price itself. For a product sold by weight (price_is_average is true) it is Pick n Pay's estimate for one pack. |
was_price, savings | The price before the markdown and the rand saving, when Pick n Pay prints them. Null when it does not, never 0. |
on_promotion | True when Pick n Pay marks the shelf price as a promotion price. It is what Pick n Pay says, not a comparison against price history. Many Smart Shopper offers leave the shelf price alone and show only in promotions. |
price_is_average | True for products sold by weight, where price is an estimate for one pack. |
unit_price, unit_price_per | The price per unit of measure when Pick n Pay prints one (79.99 per 1kg). Null for both when it prints none. |
promotions, promotion_ends | The promotion messages attached to the product as Pick n Pay words them (Combo For R70.00, R17.49, Collectaboks: Buy 2 For R90.00), and the earliest end date among them (YYYY-MM-DD, UTC). Mostly Smart Shopper (loyalty) offers. Empty list and null when there are none. |
badges | The text badges on the product tile (SAVE, SMART SHOPPER, APP DEAL, Online Only). Empty when none. |
in_stock, stock_status | Whether Pick n Pay reports the product in stock in this store, and its status word (inStock, outOfStock). Out-of-stock products are still listed, with their price. |
rating, review_count | The average star rating (1 to 5) and the number of reviews. Rating is null for a product with no reviews (about 99% of rows: most products have none); review_count is then 0. |
sponsored | True when the listing is a paid placement. |
scraped_at | When the row was read, UTC. |
Example row (real values, 2026-11-05, the bakery category on promotion, store WC44):
{"search_term": null,"category": "bakery-423144840","sort": "relevance","position": 2,"store_code": "WC44","code": "000000000000214534_EA","base_code": "000000000000214534","unit_code": "EA","name": "PnP Dessert Topped Buns 6 Pack","url": "https://www.pnp.co.za/All-Products/Bakery/Cakes-%26-Treats/Cakes/pnp-dessert-topped-buns-6-pack/p/000000000000214534_EA","image_url": "https://cdn-prd-02.pnp.co.za/sys-master/images/h04/hba/11470151876638/silo-product-image-v2-21Aug2024-180128-2005523000000-Straight_on-242177-1260_400Wx400H","supplier": "PICK N PAY","category_path": ["Bakery", "Cakes & Treats", "Cakes"],"currency": "ZAR","price": 22.99,"was_price": 25.99,"savings": 3.0,"on_promotion": true,"price_is_average": false,"unit_price": null,"unit_price_per": null,"promotions": [],"promotion_ends": null,"badges": ["SAVE"],"in_stock": true,"stock_status": "inStock","rating": null,"review_count": 0,"sponsored": false,"scraped_at": "2026-11-04T20:11:43Z"}
Honest nulls, and what the search does not carry
A field the source did not give is null, never 0 or "". promotions,
badges and category_path are empty lists when there are none.
- Prices are per store. The same product costs different amounts in different
stores: 6 x 1L UHT milk was R97.99 in
WC44and R99.99 inEC01on 2026-11-05. Many store codes do not exist (GP01,KZ01andFS01were refused); the run fails with an error naming the code rather than returning another store's prices. - The search is fuzzy. A term Pick n Pay has no product for still returns loosely
related products (
zzqqxxreturned 16, cucumber, tomatoes and bananas first). A term inside a category that has none of it returns nothing (laptopinbakery), is reported in the log and costs nothing. Checksearch_termandnamewhen a term is unusual. - A term does not contain
:. It separates the filters in Pick n Pay's query, so inside a term it is read as a space. - Promotions are what the search shows.
promotionsandpromotion_endscome from the offers attached to the product in the search result; they do not say whether a Smart Shopper card is needed.on_promotionfollows the shelf price. TheonPromotionOnlyfilter is Pick n Pay's own On Promotion filter, which also lists products whose only promotion is a Smart Shopper offer. - No brand, ingredients or nutrition. The search result carries the supplier and the product tile, not the product page's description, brand, ingredients or nutrition table. This actor does not read the product pages.
- At most 10,000 products per search. Pick n Pay's search ends at 10,000 results (100 pages of 100); the log names a search that has more and says to narrow it by category or by the promotion filter.
- A product is written once per search. A repeat the live order shows on a later
page is not written or charged again. The same product found by two different search
terms is written once for each, with its own
search_term.
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.00105 per product written (
product-scraped)
A row that is not written is not charged: a repeat of a product the same search already returned, and a search that fails, are free.
How it fetches
pnp.co.za is a storefront on SAP Commerce (Hybris) whose pages call its own API.
This actor calls the same one: POST https://www.pnp.co.za/pnphybris/v2/pnp-spa/products/search
with the search in the query string (query=milk:relevance, pageSize=100,
currentPage=0, storeCode=WC44) and an Accept: application/json header. It asks for
fields=products(FULL),pagination(DEFAULT), which is the products and the pager only:
the default reply adds every filter facet of the search and is 298 KB for two
products. A browser's own Accept header is answered with XML, so the header is sent
explicitly. The same address fetched with GET is refused (HTTP 400: Pick n Pay reads
search as a product code).
Things worth knowing, all measured on 2026-11-05:
- Reachable from Apify. The POST was answered with JSON 10 of 10 times through Apify's datacenter proxy, 10 of 10 from the platform's own address and 10 of 10 through residential (a throwaway probe run on the Apify platform, a browser TLS fingerprint, the same request this actor makes). A refused request is retried from a fresh address.
- Paging. 100 products per page at most (a larger
pageSizeis answered with 100) and the reply says how many results and pages there are. A request for a smallmaxItemsasks for a small page. A page past the end is HTTP 200 with no products. - Categories. A term inside a category is
milk:relevance:category:<code>; a whole category with no term is:relevance:allCategories:<code>. The two forms are not interchangeable on Pick n Pay's side, and this actor uses the right one for each. An unknown category code fails the run. - Sort and promotion filter.
price-ascandprice-descreorder the whole result; the On Promotion filter narrowedmilkfrom 1,249 to 304 products. - Weight. About 1.9 KB per product, so a 30-product run is one request and a second or two.
If Pick n Pay changes the API or its reply, the run fails with a message instead of returning empty rows.