Pick n Pay Scraper: South Africa Grocery Prices & Promotions avatar

Pick n Pay Scraper: South Africa Grocery Prices & Promotions

Pricing

$1.50 / 1,000 products

Go to Apify Store
Pick n Pay Scraper: South Africa Grocery Prices & Promotions

Pick n Pay Scraper: South Africa Grocery Prices & Promotions

Scrape pnp.co.za categories: price in ZAR, crossed-out price, price per kg, every promotion (SAVE, Smart Shopper), stock, rating; optional product pages for brand, barcode, ingredients, allergens. Pay per result.

Pricing

$1.50 / 1,000 products

Rating

0.0

(0)

Developer

Hexagone Data

Hexagone Data

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

Pick n Pay Scraper — South African Grocery Prices & Promotions

Collect product information from pnp.co.za (Pick n Pay, one of South Africa's biggest supermarket chains). Paste category URLs and get every product of the category, paginated to the end of the list, with prices in South African rand, promotions, ratings, stock and the price context the site applied. Enable Open each product page for brand, barcode and ingredients to also get the consumer brand, barcode, description, ingredients and allergens of every delivered product.

Use dated exports to track an assortment or monitor promotions over time.

Try it with 20 products

  1. Paste a category URL into Start URLs.
  2. Set Maximum number of products to 20.
  3. Start the Actor and open its dataset.
  4. Read Run summary before exporting JSON, CSV or Excel: a successful process can still contain incomplete URLs or check warnings.

For the Apify API, a valid input for the steps above is:

{
"startUrls": [{"url": "https://www.pnp.co.za/c/milk-dairy-and-eggs-423144840"}],
"maxItems": 20
}

The form's product-count prefill is 20, while the schema default is 500. Set maxItems explicitly in API calls; 0 removes the product-count limit. Categories hold hundreds to thousands of products (Milk, Dairy & Eggs: ~980 at the time of writing): start small.

What to paste

InputDataNotes
Category https://www.pnp.co.za/c/milk-dairy-and-eggs-423144840Every product of the category, paginated to the end of the list (72 products per page)The category code is the last part of the URL. Sub-categories have their own /c/… page and can be pasted too.

You can paste several categories; they are scraped one after the other and deduplicated within the run. Anything else — the home page, search pages, product pages, another domain, URLs with parameters — is rejected with a reason and never blocks the other URLs.

Supplier is not the brand — and how to get the brand

The category listings carry supplier: the supplier/distributor registered by Pick n Pay (brandSellerId), which is regularly not the consumer brand shown on the page ("LACTALIS SA" is the supplier of Parmalat products, "WINDMEUL EIERBOERE P/L" of Humpty Dumpty). Treat supplier as provenance data, never as the brand.

The consumer brand lives on each product's page, which the Actor can open for you: tick Open each product page for brand, barcode and ingredients (includeDetails). For every delivered product (within your maxItems), one extra request fills brand, barcode (usually EAN-13), description, ingredients, allergens, features, certifications and categoryPath. Without the option those fields are null — they do not exist in the category listings.

Complete promotions need the details option too

Without the details option, the promotion fields can be incomplete. The category listing caps its promotions at one per product and sometimes omits one entirely: on 25 October 2026, 12 of 150 measured products had at least one more promotion on their product page (including a public "2 For R65.00" SAVE special that the listing did not show, and one promotion created the day before). Prices are not affected — they matched on all 150 products.

With the option enabled, the promotion fields (promotions, promotion, promoPrice, promotionType, promotionRequiresLoyaltyCard, promotionEndDate) and inStock come from the product's own page: promotions are complete there, and the stock is what the page displays. The detailsSource field says which it is: listing (potentially incomplete promotions, listing stock) or productPage (complete promotions, page stock). If you analyze promotions or stock, enable the option and filter on detailsSource: "productPage". promotionsSource is an alias of detailsSource with the same value, kept so existing exports are not broken.

Without the option, stock comes from the listing too and can disagree with the product page: on 25 October 2026, 1 of 19 weighed items was shown in stock by the listing while its page said "Currently out of stock" (and 12 of 150 products had at least one promotion missing from the listing).

Allergens are a list

allergens is a list of texts, exactly as the product page shows them (for example ["Milk", "Soya"] for Rama 37% Fat Spread, measured 25 October 2026). It is null when the page shows none. features (marketing highlights) and certifications (e.g. Kosher, Halaal) follow the same shape, and allergenWarnings carries the page's separate "Allergen Warnings" section the same way — never merged into allergens.

The option costs about 2 extra seconds per delivered product (one request per product, 2 seconds apart) and changes nothing to the pricing: you are still charged per delivered product, one dataset event each. A product page that fails (unknown code, refusal, unreadable response) never blocks anything: the product is delivered without details, and the run summary counts detailPages and detailErrors; if every product page fails, a check warning says so.

Prices are the site's default-store prices

The Actor browses like a signed-out visitor, and the site then applies its default store (WC21) to every price. The store applied is written in each record's priceContext. These are not the prices of a store you chose, and they are not a national average: compare exports only with that context in mind. Pick n Pay's API refuses store codes it does not know, so no store can be selected.

Promotions and Smart Shopper prices

price is the price the site displays to a visitor without a loyalty card — when the site marks the price as a promotion, oldPrice is the crossed-out price. A promotion can exist without changing price: on 25 October 2026, 2 of 991 real products carried a public SAVE offer phrased "1 For R39.99" while their price stayed at 47.99 (promoPrice stays null because the offer's text is not a plain "R…" price — see promotions for the offer itself). price is per kg when priceUnit is "kg" (loose, weighed items).

A product can carry one or several promotions. promotions lists every promotion the record's source knows (each with text, type, requiresLoyaltyCard, startDate, endDate); the flat fields promotion, promotionType, promotionRequiresLoyaltyCard and promotionEndDate describe the first one, and promotions is an empty list when there is none — see the complete-promotions section above for what each source covers.

The same offer can appear twice in promotions: once as a public offer and once as a loyalty-card offer. Pick n Pay's API sometimes files one offer both as SAVE (public, requiresLoyaltyCard: false) and as SMART_SHOPPER (card, requiresLoyaltyCard: true), with the same text — e.g. Stork "2 For R55.00". This is not rare: 2 of 22 products with several offers on 28 October 2026, 4 of 15 on 25 October 2026. The Actor delivers both entries as the API publishes them, so if you filter "card-only offers", check whether the same text also exists as a public offer. The flat fields (promotion, promoPrice, promotionType, promotionRequiresLoyaltyCard, promotionEndDate) describe the first offer delivered, in the API's order (in the cases measured, the public SAVE entry came first); they say nothing about the second entry.

promoPrice can be a loyalty-card price — always check promotionRequiresLoyaltyCard before using it. When a promotion text is a plain price ("R89.99"), promoPrice holds that number, but most such promotions on pnp.co.za are SMART_SHOPPER: that price is reserved to holders of the Smart Shopper card, and promotionRequiresLoyaltyCard is true. It can even sit above the public promotion price (Humpty Dumpty Large Eggs 30, seen 25 October 2026: public promotion price R69.99 while the Smart Shopper price was R74.99). A SAVE promotion gives promotionRequiresLoyaltyCard: false; WEB_DEAL and unknown types give null because they do not establish whether a loyalty card is required. Multi-buy and combo texts ("2 For R34.00", "Combo For R49.99") give promoPrice: null: their unit price is never calculated.

In short: to price a product for the general public, use price; promoPrice is only the right number when promotionRequiresLoyaltyCard is false (or when your use case is explicitly the Smart Shopper price).

Output

FieldMeaning
productCodePick n Pay product code, e.g. 000000000000349246_CS.
url, imagePublic product page and main product image.
nameProduct name as shown by the site (consecutive spaces in the site's internal label are collapsed to one, nothing else changes).
supplierSupplier/distributor registered by Pick n Pay (brandSellerId) — not always the consumer brand; see the supplier section.
promotion, promotionType, promotionRequiresLoyaltyCard, promotionEndDate, promoPrice, promotionsThe product's promotions, described by detailsSource (alias promotionsSource): from the listing (capped at one, can omit some) or from the product page (complete, details option). See the promotion section.
detailsSourcelisting or productPage: where the promotions and the stock of this record come from.
brand, barcode, description, ingredients, allergens, allergenWarnings, features, certifications, categoryPathDetails from each product page: only filled when the details option is enabled, else null. allergens, allergenWarnings, features and certifications are lists; categoryPath is the product page's category path (the always-filled category is the listing page's).
price, priceUnit, currency, priceDisplayPrice in rand, always ZAR, and the price exactly as the site formats it (R97.99). price is per kilogram when priceUnit is "kg" (loose, weighed items — the site shows e.g. "R149.99/kg"); priceUnit is null for a per-item price and applies to oldPrice too.
oldPriceCrossed-out price on promoted products, else null.
inStockWhether the site shows the product in stock for the default store.
rating, reviewCountAverage rating out of 5 and review count; most products carry none (rating: null, reviewCount: 0).
quantityTypeNORMAL or RESTRICTED_ALLOWED_QUANTITY (purchase limit the site applies).
categoryCategory path of the listing page, e.g. All Products > Milk, Dairy & Eggs.
priceContextStore the site applied to a signed-out visitor (see above).
position, page, sourceUrl, scrapedAtCollection provenance. scrapedAt is when the category listing was read: the time of price, oldPrice, priceDisplay and the other listing fields.
detailsScrapedAtWhen this product's page was read (details option): the time of promotions, inStock and the page details, which can be read up to tens of minutes after the listing on large categories. null when the record carries no product-page data (detailsSource: "listing").

A field the site does not publish is null, never an empty string.

priceContext (an object) and promotions (a list of objects) aside, records are flat. Account for both when transforming JSON or choosing spreadsheet columns. Retain scrapedAt with your exports: this Actor returns observations, not a maintained history of price changes.

Real output example

The following record is a real record delivered by build 0.1.8 (run yeRafCkwWJP0Im2c0, collected on 25 October 2026, details option enabled; taken as-is from the run's dataset, except detailsScrapedAt, a field added in build 0.1.15 whose value here is illustrative). It demonstrates the format, not today's price or stock.

{
"productCode": "000000000000251804_EA",
"url": "https://www.pnp.co.za/All-Products/Milk%2C-Dairy-%26-Eggs/Butter-%26-Margarine/Butter-%26-Butter-Spreads/springbok-unsalted-butter-500g/p/000000000000251804_EA",
"name": "Springbok Unsalted Butter 500g",
"supplier": "CLOVER SA LTD",
"brand": "SPRINGBOK",
"barcode": "6001299810213",
"description": "Enjoy the delicious taste of Clover Springbok Unsalted Butter in your baking, cooking, or spread on bread.",
"ingredients": "Cream (cow’s milk)",
"allergens": [
"Milk"
],
"allergenWarnings": null,
"features": [
"Creamy texture",
"Ideal for precise baking measurements",
"Enhances the flavour and texture of baked goods",
"Versatile for various recipes",
"Consistent quality and taste"
],
"certifications": [
"Kosher",
"Halaal"
],
"categoryPath": "All Products > Milk, Dairy & Eggs > Butter & Margarine > Butter & Butter Spreads",
"price": 99.99,
"priceUnit": null,
"currency": "ZAR",
"priceDisplay": "R99.99",
"oldPrice": null,
"promotionsSource": "productPage",
"detailsSource": "productPage",
"inStock": true,
"rating": null,
"reviewCount": 0,
"image": "https://cdn-prd-02.pnp.co.za/sys-master/images/haa/hca/11602819645470/silo-product-image-v2-11Mar2025-180120-6001299810213-Straight_on-296887-216_400Wx400H",
"quantityType": "NORMAL",
"category": "All Products > Milk, Dairy & Eggs",
"priceContext": {
"store": "WC21",
"note": "default store for anonymous visitors"
},
"position": 36,
"page": 2,
"sourceUrl": "https://www.pnp.co.za/c/milk-dairy-and-eggs-423144840",
"scrapedAt": "2026-10-25T03:59:23+00:00",
"detailsScrapedAt": "2026-10-25T04:02:11+00:00",
"promotion": "Collectaboks: Buy 2 For R165.00",
"promoPrice": null,
"promotionType": "SMART_SHOPPER",
"promotionRequiresLoyaltyCard": true,
"promotionEndDate": "2026-11-08T21:59:59+0000",
"promotions": [
{
"text": "Collectaboks: Buy 2 For R165.00",
"type": "SMART_SHOPPER",
"requiresLoyaltyCard": true,
"startDate": "2026-10-22T22:00:00+0000",
"endDate": "2026-11-08T21:59:59+0000"
}
]
}

Here price 99.99 is the price every visitor pays, while the promotion is a Smart Shopper offer: promotionRequiresLoyaltyCard is deduced from the promotion's type as Pick n Pay's API files it (SMART_SHOPPER means the loyalty card is required) — the site's page can display the same offer differently, without the card mention — this happens with offers the API publishes twice, public and card (see the promotion section: 2 of 22 multi-offer products on 28 October 2026, 4 of 15 on 25 October 2026) — so treat the flag as the API's classification rather than a screenshot of the page. Without the details option this record would carry detailsSource: "listing" with no brand, barcode, description, ingredients, allergens, features or certifications, and its single listing promotion.

Run summary

The OUTPUT record (and its readable SUMMARY page) tells, for every pasted URL, how many products were delivered and why a URL delivered nothing (no_products, not_found, blocked_by_site, …), plus the anomalies list — empty when the automatic checks found nothing unusual. A listing that stops before the total the site announces is delivered but reported as partial_collection, with an anomaly saying how many announced products are missing. Each URL's product count is the number of records actually written to the dataset: when your maximum charge per run (or a partial acceptance by the platform) stops the delivery, the URL is stopped_at_limit with "N of M products delivered", never done. A first page that answers but is not a product listing (an HTML page, an error message) is reported as unrecognized_response ("the site may have blocked the request"), not as an empty category. A product with unexpected data in the site's listing is skipped (not delivered, not charged) and counted in an ANO_PRODUIT_ECARTE anomaly; the rest of the category is delivered and the URL is partial_collection. An invalid URL never blocks the others; a URL that really fails is reported, and the run fails only if nothing at all could be collected.

Pricing

$1.50 per 1,000 delivered products, or $0.03 for 20 products and $1.50 for 1,000. You are charged per product delivered to the dataset (the apify-default-dataset-item platform event); there is no separate Actor start fee, and standard Apify platform usage is included in this price. A run delivering no products incurs no product-result charge. Set a maximum product count (maxItems) or a maximum charge for your run — both stop the collection and the charges.

Known limits

  • Default store only. No store can be selected; see the price-context section above.
  • The site's search engine caps results at 10 000. A listing that announces exactly 10 000 products may hold more than the engine reports: coverage above 10 000 products is impossible through a single query, and the run summary warns about it (ANO_PLAFOND_MOTEUR) instead of failing.
  • Categories only. Product pages, search and the home page are rejected as start URLs: records come from the category listings, and the listing carries neither the brand, nor the barcode, nor complete promotions, nor the page's stock — enable the details option for those (see the supplier and promotion sections).
  • Records are deduplicated by productCode within a run — a protection should a listing ever repeat a product across pages (no such repeat was measured: 0 on the captured pages and on 991 real products).

Proxy

Not needed by default. Only enable the proxy configuration if your runs get blocked; the Actor then retries through it, and as a last resort through Apify datacenter then South-Africa residential proxies.