# Pick n Pay API — South African grocery prices per store from pnp.co.za: price in rand, Smart Shopper card price, multi-buy and combo deals, price per kg, stock, barcode, nutrition and the store list

> Search one Pick n Pay store's online shelf: shelf price, was-price, Smart Shopper card price (kept apart from the price), multi-buy / combo deal, price per kg for goods sold by weight, stock, rating and badges per product, plus the category / brand / certification / price facets. A row carries the product's primary promotion; `product` lists every promotion.
> ReefAPI engine `pnp` · 6 endpoints · clean JSON, no scraping or browsers to manage.

## How to call
- **Endpoint:** `POST https://api.reefapi.com/pnp/v1/<action>` with a JSON body.
- **Auth:** header `x-api-key: <YOUR_REEFAPI_KEY>` — create one free (1,000 credits, no card): https://reefapi.com/signup
- **Response (every call):** `{ ok: boolean, data: ..., meta: { record_count, credits, ... }, error: { code, message } }` — branch on `ok`. Failed calls are free except verified SHEIN NOT_FOUND on product/detail and price (4 credits).
- **One key + one shared credit pool** across every ReefAPI API. Per-call credits are listed on each endpoint below.
- **Use it from an AI agent (MCP):** connect `https://api.reefapi.com/mcp` (remote streamable-http). Send the key as `Authorization: Bearer <key>`, or put it in the URL (`?key=<key>`) when the client has no header field, as ChatGPT does.

## Endpoints

### POST https://api.reefapi.com/pnp/v1/search — 2 credits
Search one Pick n Pay store's online shelf: shelf price, was-price, Smart Shopper card price (kept apart from the price), multi-buy / combo deal, price per kg for goods sold by weight, stock, rating and badges per product, plus the category / brand / certification / price facets. A row carries the product's primary promotion; `product` lists every promotion.

**Parameters:**
- `query` (string, required) — What to search for, as typed in the site's search box.
- `store_id` (string, optional) — Pick n Pay store code from the `stores` action (e.g. WC21 Constantia, GC11, NC10). Prices, deals and the assortment are the store's own and differ between stores. Without it the site's own default store is used and the answer says so (store_defaulted: true).
- `page` (integer, optional, default 1) — Page number, from 1. Past the last page the answer is an empty page.
- `page_size` (integer, optional, default 24) — Products per page, 1-48 (default 24, the site's own page).
- `sort` (enum, optional, default "relevance") — Order of the results (the site's three sorts). [one of: relevance, price_asc, price_desc]
- `brand` (string, optional) — Only this brand, exactly as the `brands` facet of a search lists it (e.g. CLOVER, PNP). An unknown brand gives an empty result.
- `category_id` (string, optional) — Category code as the `categories` facet or a product's `categories` list gives it (e.g. milk-dairy-and-eggs-423144840, cheese44805192).
- `on_promotion` (boolean, optional, default false) — Only products with a current promotion (sale, Smart Shopper price, multi-buy, combo).
- `min_price` (number, optional) — Lowest shelf price in rand (the price anyone pays, not the Smart Shopper price; per kg for goods sold by weight).
- `max_price` (number, optional) — Highest shelf price in rand (same price as min_price).
- `certification` (enum, optional) — Only products carrying this certification. [one of: halaal, kosher, free_range]

**Returns:** query, store_id, store_name, store_defaulted, total, total_is_capped, page, page_size, total_pages, sort, returned, sponsored_count, products[]{rank, product_id, title, supplier, price, price_display, price_type ('selling' | 'promotion'), price_unit ('each' | 'kg'), currency, original_price, savings, online_only_price, member_price (Smart Shopper card price), member_price_program, member_price_ends, multi_buy{kind ('multi_buy' | 'combo'), quantity, price, card_required, text, promotion_id}, promotions[]{promotion_id, type ('smart_shopper' | 'save'), card_required, text, description, kind, quantity, deal_price, starts, ends}, sold_by ('unit' | 'weight'), weight_options[]{grams, label, min_label, max_label, default}, stock_status, in_stock, max_order_quantity, rating, review_count, badges[], sponsored, image, url}, facets{categories[]{name, count, category_id}, brands[]{name, value, count}, certifications[], price_ranges[], on_promotion[], lifestyle[], unit_of_measure[]}, breadcrumbs[]

**Example request body:**
```json
{
  "query": "milk",
  "store_id": "WC21"
}
```

### POST https://api.reefapi.com/pnp/v1/category — 2 credits
Every product in one category (department or aisle) at one store, paged, with the same rows, filters and sorts as search; the `categories` facet lists the sub-categories with their codes.

**Parameters:**
- `category_id` (string, required) — Category code as the `categories` facet or a product's `categories` list gives it (e.g. milk-dairy-and-eggs-423144840, cheese44805192).
- `store_id` (string, optional) — Pick n Pay store code from the `stores` action (e.g. WC21 Constantia, GC11, NC10). Prices, deals and the assortment are the store's own and differ between stores. Without it the site's own default store is used and the answer says so (store_defaulted: true).
- `page` (integer, optional, default 1) — Page number, from 1. Past the last page the answer is an empty page.
- `page_size` (integer, optional, default 24) — Products per page, 1-48 (default 24, the site's own page).
- `sort` (enum, optional, default "relevance") — Order of the results (the site's three sorts). [one of: relevance, price_asc, price_desc]
- `brand` (string, optional) — Only this brand, exactly as the `brands` facet of a search lists it (e.g. CLOVER, PNP). An unknown brand gives an empty result.
- `on_promotion` (boolean, optional, default false) — Only products with a current promotion (sale, Smart Shopper price, multi-buy, combo).
- `min_price` (number, optional) — Lowest shelf price in rand (the price anyone pays, not the Smart Shopper price; per kg for goods sold by weight).
- `max_price` (number, optional) — Highest shelf price in rand (same price as min_price).
- `certification` (enum, optional) — Only products carrying this certification. [one of: halaal, kosher, free_range]

**Returns:** category_id, store_id, store_name, store_defaulted, total, total_is_capped, page, page_size, total_pages, sort, returned, sponsored_count, products[]{rank, product_id, title, supplier, price, price_display, price_type ('selling' | 'promotion'), price_unit ('each' | 'kg'), currency, original_price, savings, online_only_price, member_price (Smart Shopper card price), member_price_program, member_price_ends, multi_buy{kind ('multi_buy' | 'combo'), quantity, price, card_required, text, promotion_id}, promotions[]{promotion_id, type ('smart_shopper' | 'save'), card_required, text, description, kind, quantity, deal_price, starts, ends}, sold_by ('unit' | 'weight'), weight_options[]{grams, label, min_label, max_label, default}, stock_status, in_stock, max_order_quantity, rating, review_count, badges[], sponsored, image, url}, facets{categories[]{name, count, category_id}, brands[]{name, value, count}, certifications[], price_ranges[], on_promotion[], lifestyle[], unit_of_measure[]}, breadcrumbs[]

**Example request body:**
```json
{
  "category_id": "milk-dairy-and-eggs-423144840",
  "store_id": "WC21"
}
```

### POST https://api.reefapi.com/pnp/v1/promotions — 2 credits
Everything on promotion at one store right now — sale prices, Smart Shopper card prices, multi-buys and combos — optionally inside one category or for one search term.

**Parameters:**
- `store_id` (string, optional) — Pick n Pay store code from the `stores` action (e.g. WC21 Constantia, GC11, NC10). Prices, deals and the assortment are the store's own and differ between stores. Without it the site's own default store is used and the answer says so (store_defaulted: true).
- `query` (string, optional) — Only promotions whose product matches this search.
- `category_id` (string, optional) — Category code as the `categories` facet or a product's `categories` list gives it (e.g. milk-dairy-and-eggs-423144840, cheese44805192).
- `page` (integer, optional, default 1) — Page number, from 1. Past the last page the answer is an empty page.
- `page_size` (integer, optional, default 24) — Products per page, 1-48 (default 24, the site's own page).
- `sort` (enum, optional, default "relevance") — Order of the results (the site's three sorts). [one of: relevance, price_asc, price_desc]
- `brand` (string, optional) — Only this brand, exactly as the `brands` facet of a search lists it (e.g. CLOVER, PNP). An unknown brand gives an empty result.

**Returns:** query, category_id, store_id, store_name, store_defaulted, total, total_is_capped, page, page_size, total_pages, sort, returned, sponsored_count, products[]{rank, product_id, title, supplier, price, price_display, price_type ('selling' | 'promotion'), price_unit ('each' | 'kg'), currency, original_price, savings, online_only_price, member_price (Smart Shopper card price), member_price_program, member_price_ends, multi_buy{kind ('multi_buy' | 'combo'), quantity, price, card_required, text, promotion_id}, promotions[]{promotion_id, type ('smart_shopper' | 'save'), card_required, text, description, kind, quantity, deal_price, starts, ends}, sold_by ('unit' | 'weight'), weight_options[]{grams, label, min_label, max_label, default}, stock_status, in_stock, max_order_quantity, rating, review_count, badges[], sponsored, image, url}, facets{categories[]{name, count, category_id}, brands[]{name, value, count}, certifications[], price_ranges[], on_promotion[], lifestyle[], unit_of_measure[]}, breadcrumbs[]

**Example request body:**
```json
{
  "store_id": "WC21",
  "query": "coffee"
}
```

### POST https://api.reefapi.com/pnp/v1/promotion — 1 credit
One promotion by the `promotion_id` a product row carries: every product it covers at one store (a combo's whole bundle, a multi-buy's qualifying items) with the promotion's text, deal price, card requirement and dates.

**Parameters:**
- `promotion_id` (string, required) — The `promotion_id` exactly as a product row's promotions[] gives it. Promotions run for days, so an id from a recent search is the one to use.
- `store_id` (string, optional) — Pick n Pay store code from the `stores` action (e.g. WC21 Constantia, GC11, NC10). Prices, deals and the assortment are the store's own and differ between stores. Without it the site's own default store is used and the answer says so (store_defaulted: true).
- `page` (integer, optional, default 1) — Page number, from 1. Past the last page the answer is an empty page.
- `page_size` (integer, optional, default 24) — Products per page, 1-48 (default 24, the site's own page).

**Returns:** promotion_id, promotion{promotion_id, type, card_required, text, kind, quantity, deal_price, starts, ends}, store_id, store_name, store_defaulted, total, total_is_capped, page, page_size, total_pages, sort, returned, sponsored_count, products[]{rank, product_id, title, supplier, price, price_display, price_type ('selling' | 'promotion'), price_unit ('each' | 'kg'), currency, original_price, savings, online_only_price, member_price (Smart Shopper card price), member_price_program, member_price_ends, multi_buy{kind ('multi_buy' | 'combo'), quantity, price, card_required, text, promotion_id}, promotions[]{promotion_id, type ('smart_shopper' | 'save'), card_required, text, description, kind, quantity, deal_price, starts, ends}, sold_by ('unit' | 'weight'), weight_options[]{grams, label, min_label, max_label, default}, stock_status, in_stock, max_order_quantity, rating, review_count, badges[], sponsored, image, url}, facets{categories[]{name, count, category_id}, brands[]{name, value, count}, certifications[], price_ranges[], on_promotion[], lifestyle[], unit_of_measure[]}, breadcrumbs[]

**Example request body:**
```json
{
  "promotion_id": "SCRIPT20261014212454-3000723669"
}
```

### POST https://api.reefapi.com/pnp/v1/product — 2 credits
One product at one store by the `product_id` a search row returns: price, was-price, Smart Shopper price, multi-buy / combo, every promotion with dates, price per kg and allowed weights, stock level, barcode, brand, description, features, allergens, certifications, nutrition table, storage, pack variants (single / case) with their own price and stock, categories, images.

**Parameters:**
- `product_id` (string, required) — The `product_id` exactly as a search row returns it (e.g. 000000000000349246_EA, 000000000000251704_KGM), its short form 349246_EA, or a product URL ending /p/<code>.
- `store_id` (string, optional) — Pick n Pay store code from the `stores` action (e.g. WC21 Constantia, GC11, NC10). Prices, deals and the assortment are the store's own and differ between stores. Without it the site's own default store is used and the answer says so (store_defaulted: true).

**Returns:** store_id, store_name, store_defaulted, product{product_id, base_product_id, title, brand, brand_name, supplier, barcode, description, short_description, features[], allergens[], certifications[], lifestyle[], storage, usage, serving_suggestions, allergen_warnings, dimensions_mm{width, height, depth}, price, price_display, price_type, price_unit, currency, original_price, savings, online_only_price, member_price, member_price_program, member_price_ends, multi_buy{…}, promotions[]{…, validity_text}, sold_by, weight_options[], average_weight, stock_level, stock_status, in_stock, purchasable, max_order_quantity, unit_of_measure, rating, review_count, badges[], categories[]{category_id, name}, nutrition[]{nutrient, path, per_100, per_serving, serving_size}, variants[]{product_id, price, stock_level, stock_status, url}, images[], url}

**Example request body:**
```json
{
  "product_id": "000000000000349246_EA",
  "store_id": "WC21"
}
```

### POST https://api.reefapi.com/pnp/v1/stores — 2 credits
The Pick n Pay stores of the online shop with their store_id (for every other action): code, name, display name and a closed flag where the store's own name says it is closed. Narrow by `query`.

**Parameters:**
- `query` (string, optional) — Stores whose code or name contains this text (case-insensitive).
- `include_closed` (boolean, optional, default false) — Also list stores whose own name marks them closed (default false).
- `limit` (integer, optional, default 500) — At most this many stores.

**Returns:** total (stores of the online shop), matched, returned, default_store_id, stores[]{store_id, name, display_name, closed, code_prefix}

**Example request body:**
```json
{
  "query": "Cape"
}
```

## At scale
- **Volume:** 5M+ requests a day, measured at 60 requests a second across the fleet with no
  central bottleneck. Per-key limits are raised for high-volume accounts; volume pricing on request.
- **Missing a source:** tell us a site we do not cover and it becomes an engine. A customer asked
  for bestprice.gr on 21 Sep 2026 and it was in the catalog on 22 Sep.
- **Support:** 2 minute median time from a question in the live chat to the first answer. Setup
  help included, no support tier to buy.
- **One key, one credit pool** across every API. No per-site plans, no separate subscriptions.

## More
- Try it live, no code: https://reefapi.com/playground?engine=pnp
- Human docs page: https://reefapi.com/docs/pnp
- Overview page: https://reefapi.com/pnp-api
- Every ReefAPI API in one file (for your AI): https://reefapi.com/llms-full.txt
