# Athleta Scraper - Price, Markdown & New Arrival Tracker (`superslowsloth/athleta-products`) Actor

Athleta scraper: category ids or links to one row per style-color with USD price, full price, markdown dollars and percent, price type (regular, promo, markdown), stock, rating, badges, promo lines, link and photo. For activewear price, markdown and new-arrival watching.

- **URL**: https://apify.com/superslowsloth/athleta-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 $6.30 / 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

## Athleta scraper: activewear price, markdown and new-arrival tracker (USA)

This Athleta scraper reads [Athleta](https://athleta.gap.com) (the US women's
activewear retailer) category lists and returns one flat row per **style-color**:
the current price in **USD**, the full price, the markdown in dollars and
percent, whether the price is regular, promotional or a clearance markdown,
stock, rating, badges, promotion lines, the product link and the photo. Run it
on a schedule and diff the dataset to get an **activewear price, markdown and
new-arrival watch**: which Elation leggings went from 89.00 to 59.97, which
colors just landed in New Arrivals, which are down to "Very Low" stock.

No account, no API key, no login, no browser. One request returns up to 60 colors.

### Input

| Field | Notes |
|---|---|
| `categoryIds` | Athleta category ids, one per line. The id is the number after `cid=` in any athleta.gap.com browse link. Default example: `1006482` (New Arrivals). Also checked: `1023728` Sale, `3043230` Shop All Women's, `1025878` Bottoms, `1032080` Tops, `1038916` Sports Bras, `1017102` Jackets, `89745` Dresses & Jumpsuits, `1032096` Accessories. |
| `startUrls` | Category links instead of (or as well as) ids, for example `https://athleta.gap.com/browse/category.do?cid=1006482`. The id is read from `cid=`. A product link (`product.do?pid=...`) or a link to another site is refused. |
| `maxItems` | Rows to return per category (default 100, up to 10,000). |
| `onlyOnSale` | Return only colors whose price is below their full price (`on_sale`). Colors at full price are read, not returned and not charged, and do not count toward `maxItems`. |
| `proxyConfiguration` | Apify **residential** proxy by default. Needed: see "How it fetches". |

Example input:

```json
{
  "categoryIds": ["1006482", "1023728"],
  "startUrls": [{ "url": "https://athleta.gap.com/browse/new/all-womens?cid=3043230" }],
  "maxItems": 300,
  "onlyOnSale": true
}
```

A category id Athleta does not know (HTTP 404 from its API) or that is not a
number is reported in the log and skipped at no charge. A bad id or link in the
input stops the run before anything is charged.

### One row is one style-color, not one style

Athleta sells a garment per **color**. A style (`style_id`, "Elation Ultra High
Rise Legging") comes in several colors, each with its own id (`color_id`), its
own price, its own stock and its own link, and a markdown usually hits some
colors and not others: on 2026-10-29 the same legging was 89.00 in Northern
Night and 59.97 in Morning Glory. Athleta's list also counts colors, not styles
(its own "859 colors" for New Arrivals), so a row per color is what the source
gives and what a price watch needs. **`color_id` is the unique key** of a row;
`style_id` repeats once per color. To get one line per style, group by
`style_id`.

A color that two sections of a category both list (New Arrivals shows "Featured
Styles", "All New Arrivals", then "New Tops", "New Bottoms" and so on) is listed
twice by the site: New Arrivals reports 859 entries for **537 distinct colors**.
You get 537 rows, each once, under the section that listed it first (`section`),
and are charged for 537. The same holds across categories in one run.

### Output

Prices are numbers in US dollars; `currency` is `USD` (market `us`).

| Field | Notes |
|---|---|
| `category_id`, `category_name`, `section`, `position` | The category the row was read from, its name ("New Arrivals"), the section that listed the color first ("All New Arrivals", "New Tops"; null for a category without sections) and its place in the site's own order (1 is first, counted over every entry of the pages read, repeats included). |
| `style_id`, `color_id` | Athleta's style id and color id, both strings. `color_id` is unique per row and is the `pid` of the product link. |
| `name`, `color`, `color_name` | The style name, the color family (`Purple`) and the color's own name (`Northern Night`). |
| `product_type` | The site's product type, for example `womens pants`, `womens bras wireless`, `womens jackets`. |
| `url` | The product page of this color. |
| `image_url` | The main photo (the site's `P01` image), an absolute link on athleta.gap.com. |
| `price` | The current price. |
| `regular_price` | The full price. Equal to `price` when nothing is marked down. |
| `price_type` | Why `price` is what it is: `regular`, `promo` (a promotional price such as "30% Off. Price as Marked") or `markdown` (a clearance markdown, usually marked Final Sale). The site's own codes are R, P and M. |
| `on_sale` | True when `price` is below `regular_price`. |
| `discount_amount`, `percent_off` | Dollars off and the site's whole-number percent off. **Null when not on sale**, never 0. |
| `rating`, `review_count` | The style's average star rating and how many reviews it is from. **Style-level**: every color of a style carries the same pair. **`rating` is null for a style with no reviews** (the site sends 0, which is not a rating of 0; `review_count` is 0 then). Most new arrivals have none yet. |
| `inventory_count`, `inventory_status` | Units in stock for this color across sizes, and the site's word for it: `In Stock`, `Low`, `Very Low` or `Out of Stock`. |
| `promo_flags` | The color's promotion lines as sent: `30% Off. Price as Marked`, `20% Off Taken at Checkout`, `Extra 30% Off Taken at Checkout. Final Sale.`, `Online Exclusive`. **A "Taken at Checkout" discount is not in `price`**; a "Price as Marked" one is. Empty list when none. |
| `badges` | The card badges in the site's order: `Best Seller`, `New In`, `Just Reduced`, `New to Sale`, `Selling Fast`, `New Silhouette`, `Pre-order`. Empty list when none. |
| `excluded_from_promotion` | True when the style is excluded from promotions. Null if the site sends none. |
| `currency` | `USD`. |

Example row (real values, 2026-10-29, position as on the live page):

```json
{
  "category_id": "1006482",
  "category_name": "New Arrivals",
  "section": "Featured Styles",
  "position": 7,
  "style_id": "684673",
  "color_id": "684673342",
  "name": "Reform Adjustable Sports Bra A-C",
  "color": "Blue",
  "color_name": "Blue Star",
  "product_type": "womens bras wireless",
  "url": "https://athleta.gap.com/browse/product.do?pid=684673342",
  "image_url": "https://athleta.gap.com/webcontent/0062/943/812/cn62943812.jpg",
  "currency": "USD",
  "price": 29.4,
  "regular_price": 49.0,
  "price_type": "promo",
  "on_sale": true,
  "discount_amount": 19.6,
  "percent_off": 40,
  "rating": 4.6,
  "review_count": 606,
  "inventory_count": 819,
  "inventory_status": "In Stock",
  "promo_flags": ["40% Off. Price as Marked"],
  "badges": [],
  "excluded_from_promotion": false
}
```

#### Honest nulls

A field the source did not send is `null`, never `0` or `""`. `promo_flags`
and `badges` are empty lists when there are none.

- **No sizes or per-size stock.** The list carries one total per color.
- **No description, materials or variants.** This is the category list, one row per color.
- **No coupon or loyalty price.** A "20% Off Taken at Checkout" line is shown in
  `promo_flags`; the discount itself is applied in the cart and is not in `price`.
- **Prices are the US store's** (`market=us`, USD). Other markets are not read.

### Pricing

Pay per event: a small fee per run (`actor-start`), charged only after your
input is validated, so a run that fails on bad input costs nothing, and a fee
per style-color row written (`product-scraped`). A row that is not written is
not charged: rows filtered by `onlyOnSale`, and repeats of a color another
section or category already returned, are free.

### How it fetches

It calls the JSON list behind athleta.gap.com's own category pages:

```
https://api.gap.com/commerce/search/products/v2/cc?brand=at&market=us&cid=<cid>&locale=en_US&pageSize=60&pageNumber=<n>
```

Measured 2026-10-29: HTTP 200 JSON of 0.1 to 1.5 MB per page, no key or cookie
needed. From Apify **datacenter** addresses 2 of 10 requests answered and 8 were
an Akamai "Access Denied" HTTP 403; from **residential** addresses 10 of 10
answered, so residential is the default. A refusal (403, 429, 5xx), a 200 that is
not the list, a page other than the one asked for, or a dropped connection is
treated as the address being blocked, and the actor rotates to a fresh one and
retries.

Things worth knowing, all measured:

- **Paging is `pageNumber`, 0-based.** Page 0 is the first. A page past the last
  is HTTP 200 with no entries (`pageNumber=15` of New Arrivals, which has 15
  pages 0-14, returned an empty list), and the actor stops there. The page says
  which page it served; a mismatch is treated as a refusal rather than read twice.
- **60 entries per page, entries not styles.** The list of a page holds exactly
  60 colors (the last page fewer). The response also carries the other colors of
  every style it touches (254 carried for 60 listed on page 0 of New Arrivals);
  those are not rows, so you never get a color from a page it was not listed on.
- **The walk misses nothing.** Bottoms (`1025878`) read in 8 pages of 60 gave 467
  entries and 308 distinct colors; one request with `pageSize=500` (the API refuses
  1000 with HTTP 400) gave the same 467 and 308. 60 is what Athleta's own pages ask
  for, so 60 is what is sent.
- **Repeats cost pages, not money.** New Arrivals is 15 pages for 537 rows because
  the sections repeat each other. `maxItems` counts rows returned, so 100 rows may
  need 2 pages and the last rows of a repeated category need all 15.
- **Parent categories work.** `1025878` (Bottoms: 467 listed entries, 308 distinct
  colors, 8 pages), `1023728` (Sale: about 2,190 entries, 1,388 distinct colors, 37
  pages) and `3043230` (Shop All Women's: about 2,130 entries, 1,281 distinct colors,
  36 pages) list everything under them. Entry totals drift by one or two between
  requests. Read 2026-10-29 from a home connection: the 37 Sale pages took 30 seconds.
- **Not every id on the site is a list.** Some cids that appear in links
  (`1005672`, `1082880`, `1111336`, `1197664`) are not known to the API (HTTP 404)
  and are reported and skipped.
- **Depth.** At most 10,000 rows per category are read.

If Athleta changes its list format, the run fails with a message instead of
returning empty rows.

# Actor input Schema

## `categoryIds` (type: `array`):

Athleta category ids, one per line. The id is the number after cid= in an athleta.gap.com browse link. Examples that were checked: 1006482 New Arrivals, 1023728 Sale, 3043230 Shop All Women's, 1025878 Bottoms, 1032080 Tops, 1038916 Sports Bras, 1017102 Jackets, 89745 Dresses and Jumpsuits, 1032096 Accessories. Each category is read separately, 60 colors per page; a style-color that two sections or two categories both list is returned once, under the first. An id Athleta does not know is reported and skipped at no charge.

## `startUrls` (type: `array`):

Athleta category pages to read in full, for example https://athleta.gap.com/browse/category.do?cid=1006482 or https://athleta.gap.com/browse/new/all-womens?cid=3043230. The category id is taken from the cid= parameter. Product links (product.do?pid=...) and links to other sites are refused. Leave empty to use category ids only.

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

Read at most this many style-colors for each category. Athleta lists 60 per page, so 100 reads two pages. A category whose sections repeat each other (New Arrivals lists 859 entries for 537 colors) needs more pages than rows. Up to 10,000 are accepted.

## `onlyOnSale` (type: `boolean`):

Return only style-colors whose current price is below their full price (the on_sale field), which covers both promotional prices and clearance markdowns. Colors at full price are read but not returned and not charged, and do not count toward Max rows. A category with few markdowns reads many pages to find them.

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

Residential is what works. Athleta's product API sits behind Akamai: measured 2026-10-29, Apify residential exits passed 10 of 10 and datacenter exits 2 of 10 (the rest an Access Denied HTTP 403). A refused request is retried from a fresh address. A page is 0.1 to 1.5 MB, which residential traffic is billed for.

## Actor input object example

```json
{
  "categoryIds": [
    "1006482"
  ],
  "startUrls": [],
  "maxItems": 100,
  "onlyOnSale": false,
  "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 = {
    "categoryIds": [
        "1006482"
    ],
    "startUrls": [],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/athleta-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 = {
    "categoryIds": ["1006482"],
    "startUrls": [],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,superslowsloth/athleta-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/exXXcxr2xgUbM4fdb/builds/fzzhfb1xZqf3VKCMy/openapi.json
