# Shop Catalog & Price Tracker (`datafeed/shopify-catalog-tracker`) Actor

Shopify, WooCommerce and other shops: track prices and stock on a schedule in one schema (brand, GTIN/MPN/SKU, price, currency, stock, variants). Only changed products, price history kept. Public data only, robots.txt respected.

- **URL**: https://apify.com/datafeed/shopify-catalog-tracker.md
- **Developed by:** [Benjamin Cojocaru](https://apify.com/datafeed) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## Shop Catalog & Price Tracker

For Shopify, WooCommerce and other shops that publish schema.org product data.

Track the public catalog of online shops on a schedule and get a row **only for products that changed**: new and removed products, per-variant price drops and increases (with %), sale starts and ends (compare-at price), back-in-stock and out-of-stock, new and removed variants. Each row carries the product's current variants and their recent price/availability history. The catalog and the history are kept between runs in a named key-value store.

**Price:** `product-change-returned` $0.002 per product row (and per GTIN match row). `store-checked-unchanged` $0.003 per started 1,000 products when a Shopify, WooCommerce or feed store is checked and nothing is reported (and for the first, baseline check). Shops read page by page cost one request per product, so they pay `product-pages-read` instead: $0.01 per started 100 page requests on every check, changed or not, with change rows on top. Failures are free. Ten stores of about 800 products checked daily, with 20 changed products a day, cost about $0.90 + $1.20 = $2.10/month.

It reads only what each shop publishes for machines, and finds the source by itself on the first check:

| Source (`platform`) | What is read | Requests |
|---|---|---|
| `shopify` | `/products.json` (or `/collections/<handle>/products.json`) | 1 per 250 products |
| `woocommerce` | the public WooCommerce Store API `/wp-json/wc/store/v1/products` | 1 per 100 products |
| `feed` | a Google Merchant RSS/Atom feed the shop publishes (you give `feedUrl`) | 1 |
| `structured-data` | sitemaps, then each product page's schema.org `Product` data (JSON-LD or microdata) | 1 per product page, plus up to 12 sitemap requests |

`auto` (default) tries `feed` (only if you gave a `feedUrl`), then Shopify, then WooCommerce, then product pages. A shop keeps the source found on its first check. If you force a different `platform` later, the next check becomes a free new baseline.

Every product comes out in one schema: title, brand, GTIN, MPN, SKU, price, compare-at price, currency, availability, variants, URL and image. Optional, off by default: `matchByGtin` groups products that share a GTIN (EAN/UPC) across your shops into one row. This is a niche option. In our measurement it works for only **about 25 % of shops**, because only those publish valid GTINs, and only in product-page data.

robots.txt is respected and requests to one shop are 2 s apart. No browser, no proxy, no login, no cart or checkout, no customer data, no reviews. Marketplaces (Amazon, eBay, Walmart, AliExpress, Etsy, ...) are out of scope.

**Coverage, measured, not promised.** On 200 shop-like domains sampled from the Tranco top-1M list, see [COVERAGE.md](COVERAGE.md): at least one source worked on 81 of the 152 sites we were allowed to read (53 %): 51 via Shopify, 10 via WooCommerce, 20 via product-page data. A valid GTIN was published by 25 % of those shops, only in product-page JSON-LD. Neither the Shopify feed nor the WooCommerce API had one in this sample, so matching by GTIN needs `platform: "structured-data"`.

### Input

```json
{
  "stores": [
    "www.allbirds.com",
    {"domain": "shop.example.com", "collections": ["sale", "new-arrivals"], "label": "rival A"},
    {"domain": "woo-shop.example", "label": "rival B"},
    {"domain": "brand.example", "feedUrl": "https://brand.example/feeds/google.xml"}
  ],
  "matchByGtin": true,
  "reportChangeTypes": ["price_drop", "sale_started", "back_in_stock", "new_product"],
  "confirmRemovals": true
}
```

| Field | Default | Meaning |
|---|---|---|
| `stores` | - | Up to 200 domains (a URL is accepted; only the host is used). An object can set `platform`, `feedUrl` (https, on the shop's own domain) and, for Shopify, `collections`: collection handles (the part after `/collections/` in a URL). They are read from `/collections/<handle>/products.json` instead of the whole catalog. Each store and collection set has its own history. |
| `platform` | `auto` | `auto`, `shopify`, `woocommerce`, `structured-data`, `feed`. Applies to every shop without its own `platform`. |
| `matchByGtin` | false | Also output one `gtin_match` row per GTIN found in two or more shops of this run (charged like a product row). |
| `maxPagesPerStore` | 200 | `structured-data` only: product pages read per shop (one request each). Past the limit nothing is reported as removed. |
| `reportChangeTypes` | all | `new_product`, `removed_product`, `price_drop`, `price_increase`, `sale_started`, `sale_ended`, `back_in_stock`, `out_of_stock`, `new_variant`, `removed_variant`. Other types are still written to the history, just not output. |
| `confirmRemovals` | true | A product counts as removed only when it is missing on two checks in a row. Paging through a catalog that changes during the check can skip a product once. |
| `emitBaselineProducts` | false | On the first run, output every product once (status `baseline`, charged like a change row). Off: the first run returns one free `baseline_summary` row instead (see "First run"). |
| `historyPerVariant` | 30 | History points kept per variant (a point is added only when price, compare-at price or availability changed). |
| `maxProductsPerStore` | 10,000 | Products read per store (250 per request). Past the limit nothing is compared and nothing is reported as removed. |
| `monitorStoreName` | `shopify-catalog-tracker` | The named key-value store holding the catalogs. **Use one schedule per store name.** A LOCK record (3 h expiry) refuses a second run on the same store name, and it is re-read after writing so two runs that start together do not both proceed. This is best-effort, not a guarantee: the key-value store has no atomic lock, so do not rely on it to run overlapping schedules. |
| `perHostDelayMs` | 2000 | Pause between two requests to one store (min 1 s). A longer robots.txt `Crawl-delay` wins (above 10 s only one page is read). |

Schedule it in Apify Console (Schedules, e.g. daily) and add an integration (email, Slack, webhook) on "Run succeeded" that sends you the dataset: rows exist only when something changed.

### Output (one row per changed product)

```json
{
  "store": "shop.example.com",
  "label": "rival A",
  "status": "changed",
  "productId": "7340901859408",
  "title": "Women's Flip Flop - Dusty Pink",
  "url": "https://shop.example.com/products/womens-flip-flop-dusty-pink",
  "changeTypes": ["price_drop", "sale_started"],
  "changes": [
    {"variantId": "42146889039952", "variantTitle": "5", "sku": "A12513W050", "type": "price_drop", "before": "50.00", "after": "25.00", "changePct": -50.0},
    {"variantId": "42146889039952", "variantTitle": "5", "sku": "A12513W050", "type": "sale_started", "before": null, "after": {"price": "25.00", "compareAtPrice": "50.00"}}
  ],
  "variants": [{"variantId": "42146889039952", "title": "5", "sku": "A12513W050", "price": "25.00", "compareAtPrice": "50.00", "available": false, "onSale": true}],
  "history": {"42146889039952": [{"at": "2026-10-13T08:00:00+00:00", "price": "50.00", "compareAtPrice": null, "available": false}, {"at": "2026-10-14T08:00:00+00:00", "price": "25.00", "compareAtPrice": "50.00", "available": false}]},
  "checkedAt": "2026-10-14T08:00:00+00:00",
  "previousCheckAt": "2026-10-13T08:00:00+00:00",
  "error": null
}
```

Rows also carry `platform`, `brand`, `currency`, `image` and `gtins`. Each variant has `gtin` and `mpn`, which are null when the shop does not publish them. For shops read from the WooCommerce API, product pages or a feed, `productId` is the API id, the product URL or the feed id, and `url` is the shop's own product URL.

- Prices are decimal strings in the **shop's own currency**. `currency` comes from the WooCommerce API, the page's `priceCurrency` or the feed. Shopify's `/products.json` does not say which currency, so it is `null` there (usually the store's base currency).
- A GTIN is output only when its check digit is valid.
- Availability that a page does not state is `null`. Going from unknown to known is not reported as a stock change.

#### First run: the `baseline_summary` row

The first check of a store only stores its catalog, so it returns no product rows. So that the run does not look broken, it returns one free row per store with `status: "baseline_summary"`: `store`, `platform`, `storedProducts`, `storedVariants`, `sample` (the first 5 products: title, url, price, currency), `message`, `nextStep` and `charged: false`. It has no `productId`, is never a charged event, and is not the `baseline` status (that is the charged per-product row of `emitBaselineProducts`, which replaces the summary row when on). The only charge is the usual `store-checked-unchanged` (or `product-pages-read` for page-by-page shops). The run's status message reads "Baseline stored for N store(s)", and `SUMMARY.baselineSummaryRows` counts the rows. The second run with no change returns nothing; changed products come back as `changed` rows. A free `rebaseline` after a currency or domain switch returns no summary row, only a warning.

#### GTIN match rows (`matchByGtin`)

```json
{
  "status": "gtin_match", "gtin": "4006381333931", "shops": 2, "title": "Merino Beanie", "brand": "Nordwolle",
  "currency": "EUR", "cheapestStore": "feed.example",
  "offers": [
    {"store": "feed.example", "platform": "feed", "price": "19.00", "compareAtPrice": "25.00", "currency": "EUR", "available": true, "url": "https://feed.example/merino-beanie", "productId": "MB", "variantId": "MB-1", "gtin": "4006381333931"},
    {"store": "woo.example", "platform": "woocommerce", "price": "19.90", "compareAtPrice": "24.90", "currency": "EUR", "available": true, "url": "https://woo.example/product/merino-beanie/", "productId": "101", "variantId": "101", "gtin": "4006381333931"}
  ],
  "checkedAt": "2026-10-14T08:00:00+00:00", "error": null
}
```

Match rows cover every shop checked in the run, not only shops that changed. Each shop contributes one offer per GTIN. UPC-12 and its EAN-13 form (with a leading `0`) count as the same GTIN. `cheapestStore` is set only when every offer states the same currency. Prices are never converted, and no VAT is guessed. Match rows are output only when the run did not stop early.

- "On sale" means `compare_at_price` is set and higher than `price`.
- Only buyer-visible fields are compared: title, handle, variant title, SKU, price, compare-at price and availability. `updated_at`, descriptions, images and tags change all the time and are ignored.
- Failed stores give a free row with `status: "error"` and `error.code`: `robots_disallowed`, `no_product_source` (auto-detection found no source; the message lists what each source answered), `not_shopify_feed` / `not_woocommerce` / `no_sitemap` / `no_product_pages` / `no_structured_data` / `not_product_feed` (a forced source is not there), `page_errors` (more than 20 % of the product pages failed; nothing compared), `store_password_protected`, `unexpected_redirect` (the feed redirected to a bot check, login or other page), `suspected_partial_catalog` (less than half of last check's products: nothing compared; accepted if the next check shows the same count), `http_error`, `timeout`, `catalog_too_large`, `store_error`, ...
- The SUMMARY record (key-value store of the run) lists counts, charged events, stores with changes, stores whose charging is paused, truncated stores and warnings.

**Run-summary row (free).** When a run produces no other row (for example a monitor run where nothing changed, or a run stopped at once by your maximum charge or the run timeout), the Actor adds one row `{"type": "run_summary", "status": "run_summary", "message": "...", "stopReason": ..., "warnings": [...], "summary": {...}}` (`summary` is the same as the `SUMMARY` record), so the dataset is never empty. It is never charged. Skip it with `type != "run_summary"`.

### What is stored, and how billing stays fair

- Per store: a small `state-<id>` record (last check, counts, a 200-entry run timeline), plus `catalog-<id>-a|b` and `history-<id>-a|b` gzip records. A new catalog is written to the inactive slot, and only saving the state record afterwards switches to it.
- Rows are delivered (and charged with their row) **before** the check is committed. If the commit then fails, the check is not committed and the same rows are delivered again next run: a change can appear **twice**, but it is not dropped. The budget is checked before any row is pushed. If the budget still runs out part-way through a store's rows, the check is not committed and all of its rows come again next run, so the ones already delivered appear twice.
- An unchanged check, and `product-pages-read`, are charged only after the state record is saved. The budget for change rows plus page reads is checked before anything is pushed. A failed save there gives a free `store_error` row.
- Record sizes are checked **before** charging. History is trimmed (oldest points first) to fit 4 MB. A catalog that still does not fit gives a free `catalog_too_large` row.
- If a store's state cannot be saved twice in a row, its checks are free (and listed in `SUMMARY.chargingPausedStores`) until a save works again.
- Unchanged checks write only the small state record.

### Other Apify Store Actors (prices as listed on 2026-10-14)

Prices are the FREE-plan prices from the public Store API on 2026-10-14 (source: `store-evidence/shopify-and-app-reviews-2026-10-14.json`); higher plans are cheaper and prices may have changed since.

| Actor | Users (all / 30 d) | What it returns | Price listed on 2026-10-14 |
|---|---|---|---|
| autofacts/shopify | 2,347 / 84 | full catalog on every run | $0.0012 per product (down to $0.0008 on higher plans), $0.0008 per collection |
| trovevault/shopify-products-scraper | 719 / 96 | full catalog on every run | $0.001 per dataset item, plus $0.001 per start |
| webdatalabs/shopify-product-scraper | 405 / 17 | full catalog on every run | $0.01 per product (down to $0.007) |
| phantom_coder/shopify-competitor-monitor | 4 / 0 | changes only | $0.012 per change, $0.008 per store, $0.004 per 1,000 products fetched (FREE plan) |

**Price comparison.** This Actor's `product-change-returned` is $0.002 per row, which is higher than the roughly $0.001-0.0012 per product that autofacts and trovevault listed, and lower than phantom_coder's $0.012 per change. The two work differently: a catalog Actor returns, and charges for, every product on every run, and you compare the catalogs yourself. This Actor stores the catalog between runs and returns (and charges for) only the products that changed, plus $0.003 per started 1,000 products for each check. A daily check of a 1,000-product store with nothing changed costs $0.003 here; a daily full catalog from autofacts at $0.0012 per product is $1.20 for that run. On a day when most of the store changes, a full dump can cost less than the change rows: 800 changed products are $1.60 here, and 800 products at $0.0012 are $0.96.

### Limits

- **Currency, market and domain switches.** The feed has no currency field. A store can start answering in another currency or market (for example a redirect to `/en-gb/products.json`), or move to a new domain. Then the Actor treats that check as a **free new baseline** (status `rebaseline`, no rows, a warning in SUMMARY) instead of reporting every price as changed. This happens when the final host or path prefix of the feed changes, or when more than 80 % of the variants seen in both checks changed price by the same factor (at least 5 variants). The cost: a genuine store-wide sale of exactly the same percentage, on more than 80 % of the variants, is also treated as a rebaseline and not reported. A permanent domain move shows up as one warning; set the new domain in `stores` to keep things tidy.

- Variants without a price on either check never produce price events.

- **Public data only.** Some shops turn their feed off, put the store behind a password, block bots, or disallow paths in robots.txt. Those shops give free error rows. None of this is ever worked around.

- **Product pages are slow and partial.** One request per page at 2 s each is about 7 minutes for 200 pages. A missing page counts as a missing product, and `confirmRemovals` gives one check of grace. Pages rendered only by JavaScript carry no data here.

- **WooCommerce variable products** are tracked as two points, "lowest price" and "highest price" (the Store API price range). They never get a compare-at price, because the API's minimum price and minimum regular price can come from different variations. Per-variation prices are not read.

- **Currency changes.** When a product's stated currency changes (WooCommerce, page data, feed), its prices are not compared on that check. The product is re-baselined for free, and SUMMARY carries a warning.

- **Incomplete page crawls.** If any sitemap or product page was skipped (failed, disallowed by robots.txt, or without Product data), the check is treated like a capped one: products not seen are kept, not reported as removed. A product is reported removed when its page answers 404/410 on two checks in a row, or when it is missing from a complete crawl, also confirmed on two checks.

- **Capped catalogs** (`maxProductsPerStore` / `maxPagesPerStore` reached) do not report `new_product`: the window may simply have moved. A product with several offers keyed only by position (no SKU, GTIN or URL) is not diffed per variant.

- **Shopify `/products.json` has no GTIN and no currency.** GTINs for Shopify shops are available only when the theme puts them in its page JSON-LD. To get them, force `structured-data` (one request per product).

- **No inventory counts.** The public feed has `available` only (in stock or not), not quantities. Prices are the storefront's default-market prices: no per-country markets and no B2B price lists.

- Paging uses `?limit=250&page=N`. Shopify does not document this storefront feed for third parties, and it can change it. Very large catalogs (tens of thousands of products) are slow at a polite 2 s per page, so use `collections` or `maxProductsPerStore`.

- Product title or handle changes alone are not reported (the URL in the next row uses the new handle).

- Plain HTTP: no JavaScript, no proxies, and no evasion of bot protection.

### Acceptable use

- Use it for **public catalog data only**: product titles, prices, availability, variants and identifiers. It never requests customer, order, cart, checkout or account endpoints, and it never reads reviews. The data contains nothing about people, and the Actor stores nothing personal.
- Respect each store's terms and the law that applies to you (for example the EU database right and unfair-competition rules for systematic copying). Watching competitors' public prices is common practice. Republishing someone's whole catalog is a different matter.
- robots.txt (RFC 9309) is checked on every request, under the product token `DataFeedBot`. To opt your shop out, add `User-agent: DataFeedBot` + `Disallow: /`.
- The Actor reads one store at a time with at least 1 s (default 2 s) between requests, and retries a 429/503 at most once, honouring `Retry-After` up to 30 s.

### Pricing events

| Event | When | Suggested |
|---|---|---|
| `product-change-returned` | one per product row (change, or baseline row with `emitBaselineProducts`), and one per `gtin_match` row | $0.002 |
| `store-checked-unchanged` | a Shopify, WooCommerce or feed store checked with no row (baseline or no reportable change), per started 1,000 products | $0.003 |
| `product-pages-read` | a shop read page by page (`structured-data`): per started 100 requests (sitemaps + product pages), on every committed check, changed or not | $0.01 |

Publisher: remove `apify-default-dataset-item` in Console, or every row (free error rows included) is charged on top. The Actor warns in SUMMARY when it is priced.

# Actor input Schema

## `stores` (type: `array`):

Up to 200 public shop domains (Shopify, WooCommerce or any shop with schema.org product data), e.g. shop.example.com. An entry can also be an object {"domain", "platform", "feedUrl", "collections", "label"}: platform forces one source; feedUrl is a Google Merchant RSS/Atom feed the shop publishes on its own domain; collections (Shopify only) watches only those collection handles.

## `platform` (type: `string`):

auto: Shopify /products.json, then the WooCommerce Store API, then sitemaps + schema.org Product data on product pages (a feedUrl is tried first). The source found on the first check is kept for that shop. Or force one source for all shops.

## `matchByGtin` (type: `boolean`):

Adds one row per GTIN (EAN/UPC) found in two or more of the shops in this run, with every shop's price, currency and stock. Each match row is charged like a product row.

## `reportChangeTypes` (type: `array`):

Empty = all: new_product, removed_product, price_drop, price_increase, sale_started, sale_ended, back_in_stock, out_of_stock, new_variant, removed_variant. Changes of other types are still recorded in the history.

## `confirmRemovals` (type: `boolean`):

Report a product as removed only when it is missing on two checks in a row (a catalog that changes while it is paged through can skip a product once).

## `emitBaselineProducts` (type: `boolean`):

Off: the first run of a store only stores the baseline and returns one free 'baseline_summary' row (counts, a sample, next step); one store-checked-unchanged event per started 1,000 products is charged. On: every product is also output once as a 'baseline' row, charged like a change row.

## `historyPerVariant` (type: `integer`):

Price / compare-at price / availability points kept per variant (one point per change).

## `maxProductsPerStore` (type: `integer`):

Products read per store (250 per request). Products past the limit are not compared and never reported as removed.

## `monitorStoreName` (type: `string`):

Named key-value store that keeps catalogs and history between runs. Use one schedule per store name.

## `perHostDelayMs` (type: `integer`):

At least 1 s; a longer robots.txt Crawl-delay wins.

## `timeoutSecs` (type: `integer`):

How long to wait for one page or API response before treating it as failed (5-60 seconds).

## `maxPagesPerStore` (type: `integer`):

Only for shops read from product pages (one request per page, 2 s apart). Pages past the limit are not compared and never reported as removed.

## Actor input object example

```json
{
  "stores": [
    "www.allbirds.com"
  ],
  "platform": "auto",
  "matchByGtin": false,
  "confirmRemovals": true,
  "emitBaselineProducts": false,
  "historyPerVariant": 30,
  "maxProductsPerStore": 10000,
  "monitorStoreName": "shopify-catalog-tracker",
  "perHostDelayMs": 2000,
  "timeoutSecs": 20,
  "maxPagesPerStore": 200
}
```

# Actor output Schema

## `results` (type: `string`):

One row per result (see the dataset schema for fields).

## `summary` (type: `string`):

SUMMARY record: counts, warnings, stop reason and what was charged.

# 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 = {
    "stores": [
        "www.allbirds.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("datafeed/shopify-catalog-tracker").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 = { "stores": ["www.allbirds.com"] }

# Run the Actor and wait for it to finish
run = client.actor("datafeed/shopify-catalog-tracker").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 '{
  "stores": [
    "www.allbirds.com"
  ]
}' |
apify call datafeed/shopify-catalog-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,datafeed/shopify-catalog-tracker"
        }
    }
}
```

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/qI2BWuY68pmUmhxRV/builds/DgNLYAGwPJL5njzfG/openapi.json
