noon marketplace data, by category.

A read-only JSON API to noon products, prices, seller offers, sales estimates, reviews and keyword ranks. Authenticate with a bearer key and pull exactly the category you need.

The unit of sale is one category in the UAE and KSA. A subscription covers a single category in both markets.

$199 per category per month — UAE and KSA included

Open the dashboard

New here? Quickstart · Request a trial key

Quickstart

From zero to your first rows. Every request is a plain HTTPS GET with your key in the Authorization header.

  1. 1. Create an account

    Create an account or sign in. You land on Dashboard → Data API.

  2. 2. Get access to a category

    Subscribe to a category there ($199 per category per month — UAE and KSA included), or request a trial key and we turn it on for your account.

  3. 3. Create a key

    Under API keys, name the key and press Create key. It is shown once: keep it in an environment variable.

    Shell

    export NOONSELLER_API_KEY="ns_live_…"   # your key from Dashboard → Data API
  4. 4. Check the key

    curl

    curl -H "Authorization: Bearer $NOONSELLER_API_KEY" \
      "https://api.noonseller.ae/data/v1/usage"

    Response

    {
      "success": true,
      "data": {
        "tier": "paid",
        "limits": { "per_minute": 60, "per_day": 20000 },
        "today": { "requests": 1, "rows": 0 },
        "subscriptions": [
          {
            "category_l3": "Wireless Earbuds", "status": "active", "kind": "paid",
            "current_period_end": "2026-11-19T09:30:00+00:00"
          }
        ]
      },
      "error": null,
      "meta": { "next_cursor": null, "count": 1, "sample": false }
    }

    "tier": "paid" or "tier": "trial" means your data is on. "tier": "none" means the account has no active subscription yet: data endpoints answer 403 no_subscription.

  5. 5. Copy your exact category name

    curl

    curl -H "Authorization: Bearer $NOONSELLER_API_KEY" \
      "https://api.noonseller.ae/data/v1/categories"

    Response

    {
      "success": true,
      "data": [
        {
          "category_l3": "Wireless Earbuds", "country": "ae", "product_count": 1840,
          "refreshed_24h_share": 0.214, "refreshed_7d_share": 0.733
        },
        {
          "category_l3": "Wireless Earbuds", "country": "sa", "product_count": 2112,
          "refreshed_24h_share": 0.187, "refreshed_7d_share": 0.701
        }
      ],
      "error": null,
      "meta": { "next_cursor": null, "count": 2, "sample": false }
    }

    Use category_l3 exactly as listed, URL-encoded (curl --data-urlencode and Python requests params do it for you). SKUs for the per-product endpoints come from GET /products of your category. Then pick a scenario below.

Pricing and trial

$199 per category per month — UAE and KSA included

One purchase is one category (category_l3) in both markets, whatever its size. Cancel any time.

Daily refresh for your category is set up within up to 3 days after payment. Until then, refresh is rotational: products are re-crawled on a rolling cycle, not daily.

Paid keys: 60 requests per minute and 20,000 per day per account (shared by all your keys), up to 200 rows per page.

Trial key

A trial key shows a fixed sample of about 10% of the category (at most ~300 products per market), the last 14 days of history, up to 500 requests a day and 50 rows per page. Every response says "sample": true.

Request a trial key →

Request a trial key

A trial covers one category in the UAE and KSA. A trial key shows a fixed sample of about 10% of the category (at most ~300 products per market), the last 14 days of history, up to 500 requests a day and 50 rows per page. Every response says "sample": true.

We turn trials on by hand, for an existing account: create an account first, then send us its email and the category. We reply by email; once the trial is on, create your key under Dashboard → Data API.

Or email support@noonseller.ae.

Usage scenarios

Six common jobs and the requests that do them. Each assumes the Quickstart: your key in NOONSELLER_API_KEY and the exact category_l3 from GET /categories. Examples use a sample category; responses are trimmed. Every list endpoint pages with meta.next_cursor: pass it back as cursor until it is null.

1. Daily price and stock monitoring

What did every product in my category cost, and how much stock was left, on a given day?

curl

curl -H "Authorization: Bearer $NOONSELLER_API_KEY" \
  "https://api.noonseller.ae/data/v1/snapshots?category_l3=Wireless%20Earbuds&country=ae&date=2026-10-17&limit=200"

Python (pages with meta.next_cursor)

import os
import requests

BASE = "https://api.noonseller.ae/data/v1"
API_KEY = os.environ["NOONSELLER_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

params = {"category_l3": "Wireless Earbuds", "country": "ae", "date": "2026-10-17", "limit": 200}
rows = []
while True:
    r = requests.get(f"{BASE}/snapshots", params=params, headers=HEADERS, timeout=30)
    r.raise_for_status()
    body = r.json()
    rows += body["data"]
    cursor = body["meta"]["next_cursor"]
    if not cursor:
        break
    params["cursor"] = cursor

Response (trimmed)

{
  "success": true,
  "data": [
    {
      "sku": "N53412345A", "country": "ae",
      "captured_at": "2026-10-17T06:00:00Z",
      "price": 229.0, "stock": 37, "rating": 4.4,
      "review_count": 1873, "seller_count": 3, "source": "stock_scraper"
    }
  ],
  "error": null,
  "meta": { "next_cursor": "eyJsIjp7…", "count": 200, "sample": false }
}

Every product of the category appears on every day once daily refresh is set up for your category (within up to 3 days after payment); until then a day holds only the products re-crawled that day — check refreshed_24h_share in GET /categories.

2. Buy Box and seller competition

Who wins the Buy Box in my category, at what price, and who undercuts them?

All offers of the category on one day

curl

curl -H "Authorization: Bearer $NOONSELLER_API_KEY" \
  "https://api.noonseller.ae/data/v1/offers?category_l3=Wireless%20Earbuds&country=ae&date=2026-10-17&limit=200"

Python (pages with meta.next_cursor)

import os
import requests

BASE = "https://api.noonseller.ae/data/v1"
API_KEY = os.environ["NOONSELLER_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

params = {"category_l3": "Wireless Earbuds", "country": "ae", "date": "2026-10-17", "limit": 200}
rows = []
while True:
    r = requests.get(f"{BASE}/offers", params=params, headers=HEADERS, timeout=30)
    r.raise_for_status()
    body = r.json()
    rows += body["data"]
    cursor = body["meta"]["next_cursor"]
    if not cursor:
        break
    params["cursor"] = cursor

Response (trimmed)

{
  "success": true,
  "data": [
    {
      "sku": "N53412345A", "country": "ae", "sku_config": "N53412345A-1",
      "captured_at": "2026-10-17T06:00:00Z",
      "partner_code": "p-1001", "store_name": "Anker Official Store",
      "price": 249.0, "sale_price": 229.0, "effective_price": 229.0,
      "stock": 35, "is_buyable": true,
      "is_buybox_winner": true, "is_lowest_price": false,
      "partner_rating": 4.6, "num_of_rating": 1520, "badges": ["express"]
    }
  ],
  "error": null,
  "meta": { "next_cursor": "eyJsIjp7…", "count": 200, "sample": false }
}

Offer history of one product (same row shape; default range is the last 30 days)

curl

curl -H "Authorization: Bearer $NOONSELLER_API_KEY" \
  "https://api.noonseller.ae/data/v1/products/N53412345A/offers?country=ae&from=2026-10-01"

Response (trimmed)

{
  "success": true,
  "data": [
    {
      "sku": "N53412345A", "country": "ae",
      "partner_code": "p-2044", "store_name": "Gulf Gadgets Trading",
      "effective_price": 224.0, "is_buybox_winner": false, "is_lowest_price": true,
      "captured_at": "2026-10-17T06:00:00Z"
    }
  ],
  "error": null,
  "meta": { "next_cursor": null, "count": 1, "sample": false }
}

The second response is trimmed to the fields that matter here; the full row has the same fields as the first.

Every product of the category appears on every day once daily refresh is set up for your category (within up to 3 days after payment); until then a day holds only the products re-crawled that day — check refreshed_24h_share in GET /categories.

3. Product research with sales estimates

Roughly how many units does this product sell per month, and how much should I trust the number?

curl

curl -H "Authorization: Bearer $NOONSELLER_API_KEY" \
  "https://api.noonseller.ae/data/v1/products/N53412345A/sales?country=ae"

Response (trimmed)

{
  "success": true,
  "data": {
    "model": "offers-v2",
    "status": "available",
    "estimated_sales_monthly": 410,
    "estimated_revenue_monthly": 93890.0,
    "avg_daily_sales": 13.67,
    "window_start": "2026-09-18",
    "window_end": "2026-10-18",
    "observed_days": 20.0,
    "last_observed_at": "2026-10-18T21:29:54Z",
    "noon_signal": null
  },
  "error": null,
  "meta": { "next_cursor": null, "count": 1, "sample": false }
}

Statuses: available (units were observed), no_positive_change_observed (stock was watched but no sale was detected), insufficient_data (too few observations), stale (the last observation is too old). A figure is a model estimate, never an observed purchase count. Estimates rise once daily sampling starts for your category, because sparse sampling undercounts.

There is no bulk sales endpoint: one request per SKU, within your per-minute and per-day limits. Take SKUs from GET /products of your category (scenario 6).

4. Review mining

What are customers complaining about in this product right now?

curl

curl -H "Authorization: Bearer $NOONSELLER_API_KEY" \
  "https://api.noonseller.ae/data/v1/products/N53412345A/reviews?country=ae&since=2026-10-11&limit=100"

Python (pages with meta.next_cursor)

import os
import requests

BASE = "https://api.noonseller.ae/data/v1"
API_KEY = os.environ["NOONSELLER_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

params = {"country": "ae", "since": "2026-10-11", "limit": 100}
rows = []
while True:
    r = requests.get(f"{BASE}/products/N53412345A/reviews", params=params, headers=HEADERS, timeout=30)
    r.raise_for_status()
    body = r.json()
    rows += body["data"]
    cursor = body["meta"]["next_cursor"]
    if not cursor:
        break
    params["cursor"] = cursor

Response (trimmed)

{
  "success": true,
  "data": [
    {
      "uid": "rv-88121", "rating": 2,
      "title": "Left bud stopped charging",
      "comment": "Works well for a month, then the left earbud no longer charges.",
      "title_translation": null, "comment_translation": null,
      "language": "en", "review_date": "2026-10-16",
      "is_verified_purchase": true, "helpful_count": 4, "variant": "Black"
    }
  ],
  "error": null,
  "meta": { "next_cursor": null, "count": 1, "sample": false }
}

Take SKUs from GET /products of your category (scenario 6).

5. Keyword rank tracking

Where does each product rank for the searches that matter in my category?

Tracked keywords mapped to your category

curl

curl -H "Authorization: Bearer $NOONSELLER_API_KEY" \
  "https://api.noonseller.ae/data/v1/keywords?country=ae"

Response (trimmed)

{
  "success": true,
  "data": [
    {
      "keyword": "wireless earbuds", "country": "ae",
      "category_l3": "Wireless Earbuds",
      "last_captured_at": "2026-10-15"
    }
  ],
  "error": null,
  "meta": { "next_cursor": null, "count": 1, "sample": false }
}

Ranks for one keyword on its latest weekly capture

curl

curl -H "Authorization: Bearer $NOONSELLER_API_KEY" \
  "https://api.noonseller.ae/data/v1/keywords/wireless%20earbuds/ranks?country=ae&limit=200"

Python (pages with meta.next_cursor)

import os
import requests

BASE = "https://api.noonseller.ae/data/v1"
API_KEY = os.environ["NOONSELLER_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

params = {"country": "ae", "limit": 200}
rows = []
while True:
    r = requests.get(f"{BASE}/keywords/wireless%20earbuds/ranks", params=params, headers=HEADERS, timeout=30)
    r.raise_for_status()
    body = r.json()
    rows += body["data"]
    cursor = body["meta"]["next_cursor"]
    if not cursor:
        break
    params["cursor"] = cursor

Response (trimmed)

{
  "success": true,
  "data": [
    { "sku": "N53412345A", "rank_position": 1, "ad_position": null, "captured_at": "2026-10-15" },
    { "sku": "N70023981A", "rank_position": 2, "ad_position": 1, "captured_at": "2026-10-15" }
  ],
  "error": null,
  "meta": { "next_cursor": "eyJsIjp7…", "count": 2, "sample": false }
}

Ranks are weekly and cover only tracked keywords. Rank rows omit SKUs outside our catalog. A null rank_position with an ad_position is a paid-only placement.

6. Catalog sync and new products

Which products in my category are new or changed since my last sync?

curl

curl -G "https://api.noonseller.ae/data/v1/products" \
  -H "Authorization: Bearer $NOONSELLER_API_KEY" \
  --data-urlencode "category_l3=Wireless Earbuds" \
  --data-urlencode "country=ae" \
  --data-urlencode "updated_since=2026-10-17T00:00:00+04:00" \
  --data-urlencode "limit=200"

Python (pages with meta.next_cursor)

import os
import requests

BASE = "https://api.noonseller.ae/data/v1"
API_KEY = os.environ["NOONSELLER_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

params = {"category_l3": "Wireless Earbuds", "country": "ae", "updated_since": "2026-10-17T00:00:00+04:00", "limit": 200}
rows = []
while True:
    r = requests.get(f"{BASE}/products", params=params, headers=HEADERS, timeout=30)
    r.raise_for_status()
    body = r.json()
    rows += body["data"]
    cursor = body["meta"]["next_cursor"]
    if not cursor:
        break
    params["cursor"] = cursor

Response (trimmed)

{
  "success": true,
  "data": [
    {
      "sku": "N53412345A", "country": "ae",
      "title": "Wireless Earbuds Pro 2 with ANC", "brand": "Anker",
      "category_l3": "Wireless Earbuds",
      "current_price": 229.0, "was_price": 269.0, "currency": "AED",
      "rating": 4.4, "review_count": 1873,
      "seller_name": "Anker Official Store", "seller_count": 3, "is_fbn": true,
      "category_rank": 12, "stock_left": 40,
      "noon_url": "https://www.noon.com/uae-en/N53412345A/p/",
      "updated_at": "2026-10-18T03:00:00Z"
    }
  ],
  "error": null,
  "meta": { "next_cursor": "eyJsIjp7…", "count": 200, "sample": false }
}

A + in a timezone offset must be sent as %2B (or use Z). --data-urlencode and the requests library do this for you; a hand-built URL does not.

Endpoints

Base URL https://api.noonseller.ae/data/v1. Every response uses one envelope: success, data, error, meta.

GET /categoriesCategories in scope with product count and freshness
GET /productsProducts of one subscribed category and country (cursor paging, updated_since)
GET /products/{sku}One product with its latest snapshot
GET /products/{sku}/historySnapshot history of one product
GET /snapshotsPrice and stock snapshots of one category for one UTC day
GET /offersSeller offers of one category captured on one UTC day
GET /products/{sku}/offersSeller offer history of one product
GET /products/{sku}/salesEstimated monthly sales, always with a status
GET /products/{sku}/reviewsWritten reviews of one product, newest first
GET /keywordsTracked keywords mapped to your categories
GET /keywords/{keyword}/ranksProduct ranks for one keyword on one capture date
GET /usageKey tier, limits, today’s usage and subscriptions

When filtering with updated_since, a + in a timezone offset must be sent as %2B (or use Z), otherwise it is read as a space.

Full OpenAPI documentation

Errors

An error comes in the same envelope: success: false and error: { code, message }. Branch on the code; the message is for people.

HTTPerror.codeWhat to do
401unauthorizedThe key is missing, mistyped or revoked. Send it as Authorization: Bearer <key>.
403no_subscriptionThe account has no active subscription or trial yet. Subscribe in the dashboard or request a trial key.
403out_of_scopeThat category and country are not in your subscription, or (trial keys) the product is outside your sample.
400invalid_requestA parameter is wrong; the message names it. Trial keys see only the last 14 days.
400invalid_cursorSend meta.next_cursor back exactly as you received it, with the same filters.
404not_foundNo such product (SKU) or keyword in that country, or a wrong path.
429rate_limitedOver the per-minute or per-day limit, shared by all keys of the account. Wait the number of seconds in the Retry-After header.

What the data is, and is not

  • Refresh is rotational until daily refresh is set up. Products are re-crawled on a rolling cycle, not daily per SKU. GET /categories shows the real freshness of each category you can read.
  • No zero-review products. Products with no reviews are not in the dataset, by design. In fashion these are mostly new arrivals.
  • Sales are model estimates. Every sales figure is a model estimate (offers-v2) and carries a status. It is never an observed purchase count. Estimates rise once daily sampling starts for your category, because sparse sampling undercounts.
  • History starts where each table starts. As of 2026-10-18: product snapshots from 2026-03-31, seller offers from 2026-06-25, keyword ranks from 2026-04-05. There is no backfill before that.
  • Keyword ranks are weekly. They cover only the keywords we track, not every search on noon. Rank rows omit SKUs outside our catalog, so not every ranked SKU appears.
  • No sub-type split. The unit of sale is the category. Sub-types such as dresses or tops are not sold separately.

Terms of use

  • Internal research only. The data is for your own research and analysis. No resale or redistribution.
  • Keys can be revoked. We can revoke a key that is misused.
  • No uptime guarantee. The API is provided as is. A missed collection day can leave a gap in the history.
  • Cancellation. You can cancel any time; access continues to the end of the paid period, and no refunds are given.