# Shopify Store Scraper - Prices, Stock & Change Tracking (`cedarfetch/shopify-store-scraper`) Actor

Extract the full public product catalog of any Shopify store: products, variants, SKUs, prices, compare-at prices, stock and images. Built-in change tracking flags new, removed, price-changed and restocked products between runs.

- **URL**: https://apify.com/cedarfetch/shopify-store-scraper.md
- **Developed by:** [Cedarfetch Labs](https://apify.com/cedarfetch) (community)
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 products

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

## Shopify Store Scraper - Prices, Stock & Change Tracking

Extract the complete public product catalog of any Shopify store: products, variants, SKUs, prices, compare-at (sale) prices, stock status, images, tags, vendors and descriptions. Run it on a schedule with **change tracking** on and every row tells you what happened since the last run: new product, price changed, back in stock, out of stock, unchanged, or removed.

It reads only the public catalog endpoints that every Shopify storefront serves to every visitor (`/products.json`, `/collections/.../products.json`, `/products/<handle>.js`, `/meta.json`, `/sitemap.xml`). No browser, no login, no proxies needed for normal stores, so runs are fast and cheap.

### What you can use it for

- **Price monitoring**: track competitor prices and discounts per SKU, get only the changes each day.
- **Stock and restock alerts**: see which products sold out or came back.
- **New product launches**: catch new arrivals the day they appear.
- **Catalog research and dropshipping**: export a full store catalog to a spreadsheet in one click.
- **Market and assortment analysis**: vendors, product types, tags, price ranges across many stores.
- **AI agents and pipelines**: clean, typed JSON with stable field names.

### Why this one

- **Change tracking built in.** Other Shopify scrapers give you a snapshot and leave the diffing to you. This Actor keeps a compact snapshot per store in your account and labels every row, including rows for products that disappeared. Turn on "Output only changed products" and you pay only for what changed.
- **Complete catalogs.** Shopify serves at most 25,000 products through `/products.json` (100 pages of 250). Above that, and on stores that disable `/products.json`, the Actor automatically continues through the store's sitemap and per-product endpoints.
- **Clean, typed output.** Prices are numbers, not strings. Descriptions come as plain text and as the original HTML. Sale price, discount percent and an availability summary are computed for you.
- **Spreadsheet mode.** One row per variant with options in columns (`option1Name`, `option1Value` ...), tags as text, no nested objects.
- **Honest failures.** Password-protected, blocked, non-Shopify and misspelled stores are reported per store with a plain explanation, never as a silent empty dataset. If no products at all could be extracted, the run fails with the reason.
- **Polite by default.** 2 parallel requests per store, a pause between requests, backoff on rate limits.

### Input

Only `storeUrls` is required. Paste stores in any form:

| You paste | What is read |
|---|---|
| `allbirds.com`, `https://www.allbirds.com/` | The whole store |
| `store-name.myshopify.com` | The whole store (redirects to its real domain are followed) |
| `https://kith.com/collections/footwear` | Only that collection |
| `https://shop.com/products/blue-shirt` | Only that product |

#### Example: whole catalog of two stores

```json
{
    "storeUrls": ["allbirds.com", "www.deathwishcoffee.com"]
}
```

#### Example: daily price and stock feed, changes only, one row per variant

```json
{
    "storeUrls": ["allbirds.com", "kith.com/collections/footwear"],
    "outputMode": "variants",
    "trackChanges": true,
    "changesOnly": true
}
```

#### Example: products updated in the last 7 days, with collections

```json
{
    "storeUrls": ["www.deathwishcoffee.com"],
    "updatedAfter": "7 days",
    "includeCollections": true
}
```

#### All options

| Field | Default | What it does |
|---|---|---|
| `storeUrls` | (required) | Stores, collections or products to read. |
| `maxProductsPerStore` | 0 (all) | Stop after this many products per store. |
| `outputMode` | `products` | `products`: one row per product, variants nested. `variants`: one row per variant, flat. |
| `collectionHandles` | none | Read only these collections (handles from the URL) in every store. |
| `updatedAfter` | none | Keep products with `updated_at` on or after an ISO date or a relative value (`7 days`, `24 hours`). |
| `trackChanges` | false | Compare with the previous run and label every row. See below. |
| `changesOnly` | false | With tracking on, skip unchanged rows. |
| `snapshotStoreName` | `shopify-store-scraper-snapshots` | Named key-value store that holds the snapshots. Use different names for independent tracking jobs. |
| `includeCollections` | false | Attach the list of collections each product belongs to. Extra requests. |
| `maxCollectionsPerStore` | 100 | Cap on collections mapped when the above is on. |
| `enrichVariants` | false | One extra request per product to add barcodes and, where the store publishes it, `inventoryQuantity`. |
| `concurrency` | 2 | Parallel requests per store. |
| `requestDelayMs` | 300 | Pause after each request to the same store. |
| `maxConcurrentStores` | 3 | Stores crawled at the same time. |
| `requestTimeoutSecs` | 30 | Timeout for one request. |
| `maxRetries` | 4 | Retries for 429, 5xx and network errors. |

### Output

Rows go to the default dataset. Download them as JSON, CSV, Excel, XML or RSS from the Output tab or the API. The dataset has four views: **Products**, **Changes**, **Catalog details** and **Stores that could not be read**.

#### Products mode (one row per product)

```json
{
    "recordType": "product",
    "storeDomain": "www.allbirds.com",
    "productId": 7215082405968,
    "handle": "womens-wool-runner-nz-shadow-blue-natural-white-sole",
    "url": "https://www.allbirds.com/products/womens-wool-runner-nz-shadow-blue-natural-white-sole",
    "title": "Women's Wool Runner NZ - Shadow Blue (Natural White Sole)",
    "vendor": "Allbirds",
    "productType": null,
    "tags": ["11/3/26 DROP", "..."],
    "createdAt": "2025-08-27T09:16:36-07:00",
    "updatedAt": "2026-11-11T08:31:27-08:00",
    "publishedAt": "2026-11-04T15:32:48-08:00",
    "descriptionText": "(the product description as plain text)",
    "descriptionHtml": "<p>(the original description HTML)</p>",
    "options": [{ "name": "Size", "position": 1, "values": ["5", "5.5", "6", "..."] }],
    "images": ["https://cdn.shopify.com/s/files/1/1104/4168/files/A12799W-SHADOWBLUE_SIDE_TRANSPARENT.png?v=1793835066", "..."],
    "featuredImage": "https://cdn.shopify.com/s/files/1/1104/4168/files/A12799W-SHADOWBLUE_SIDE_TRANSPARENT.png?v=1793835066",
    "imageCount": 4,
    "currency": "USD",
    "priceMin": 120,
    "priceMax": 120,
    "compareAtPriceMin": null,
    "compareAtPriceMax": null,
    "onSale": false,
    "discountPercent": 0,
    "available": true,
    "availabilityStatus": "partially_available",
    "variantCount": 13,
    "availableVariantCount": 12,
    "variants": [
        {
            "variantId": 41314103427152,
            "title": "5",
            "sku": "A12799W050",
            "options": { "Size": "5" },
            "option1": "5", "option2": null, "option3": null,
            "price": 120,
            "compareAtPrice": null,
            "onSale": false,
            "discountPercent": 0,
            "available": true,
            "grams": 1000,
            "barcode": null,
            "requiresShipping": true,
            "taxable": true,
            "position": 1,
            "imageUrl": null,
            "createdAt": "2025-08-27T09:16:36-07:00",
            "updatedAt": "2026-11-11T08:31:27-08:00"
        }
    ],
    "source": "products_json",
    "scrapedAt": "2026-11-11T16:42:56.406Z"
}
```

(Values shortened for the example.) `availabilityStatus` is `in_stock` (every variant purchasable), `partially_available` or `out_of_stock`. `discountPercent` is the largest discount among variants that have a compare-at price above their price. `barcode` stays `null` unless `enrichVariants` is on and the store exposes it.

#### Variants mode (one row per variant)

Same product fields, flattened: `productTitle`, `variantTitle`, `sku`, `barcode`, `option1Name` / `option1Value` (up to 3), `price`, `compareAtPrice`, `onSale`, `discountPercent`, `available`, `grams`, `imageUrl`, `tags` (comma-separated), `collections` (comma-separated), `url` with `?variant=<id>`. No nested objects, so CSV and Excel exports are clean.

#### Stores that could not be read

A row with `recordType: "store_status"` is added for each store that could not be read, with a `status` and a plain-language `message`:

| status | Meaning |
|---|---|
| `password_protected` | The store is behind Shopify's storefront password. Not supported. |
| `blocked` | The store refused access (401/403) or showed a bot-protection page. Skipped, never worked around. |
| `rate_limited` | The store kept answering 429 after polite retries. Try later or lower `concurrency`. |
| `not_shopify` | The site does not serve Shopify's public catalog endpoints. |
| `store_unavailable` | Shopify says the shop is closed or the domain is not connected. |
| `unreachable` | The domain does not resolve, or TLS/network failed. |
| `invalid_input` | The value is not a usable domain or URL. |
| `no_products` | A Shopify store with an empty public catalog (or an empty collection selection). |

These rows are never charged.

#### Run summary

The default key-value store record `OUTPUT` holds a summary: per store the status, products found and delivered, rows, requests, retries, duration, currency, whether the catalog was read completely, warnings, and change counts. Use it to monitor scheduled runs.

### Change tracking

Turn on **Track changes between runs** and schedule the Actor (for example daily). Each run compares every product with the snapshot from the previous run of the same store and adds:

| Field | Meaning |
|---|---|
| `changeType` | The most important change: `new`, `price_changed`, `back_in_stock`, `out_of_stock`, `unchanged` or `removed`. |
| `changes` | Every change that applies, e.g. `["price_changed", "out_of_stock"]`. Products mode also lists variant-level events: `variants_added`, `variants_removed`, `variant_sold_out`, `variant_restocked`. |
| `previousPriceMin`, `previousPriceMax` | Last run's price range (products mode). |
| `previousPrice` | Last run's price (variants mode). |
| `previousAvailable` | Whether it was purchasable last run. |
| `changeDetails` | Products mode: the variant IDs behind each change. |

Rules, so you can rely on the labels:

- **The first tracked run labels everything `new`.** It creates the baseline.
- **Priority** when several things changed: `price_changed`, then `back_in_stock`, then `out_of_stock`. The `changes` list always has all of them.
- **Product-level stock** means "any variant purchasable". If one size sells out but others remain, the product stays `unchanged` and `changes` contains `variant_sold_out`. In variants mode each variant gets its own label.
- **Removed** rows are emitted for products (or variants) that were in the snapshot but are no longer in the store, with their previous price. They are reported **only after a complete read** of the same scope. If a run was capped by `maxProductsPerStore`, hit an error, or stopped at the charge limit, nothing is labelled removed, and the next complete run reports it.
- **Nothing is lost on a budget stop.** The snapshot is updated only for rows actually delivered, so a change that did not fit in this run is reported in the next one.
- **Scopes are tracked separately.** A whole-store run and a run on `collections/sale` of the same store keep separate snapshots.
- `updatedAfter` filters the output but still reads the whole catalog, so removals are still detected.
- Snapshots live in the named key-value store `snapshotStoreName` in your Apify account (compressed, one record per store and scope). Delete that store, or pick a new name, to start over.

### Pricing

This Actor uses pay-per-event pricing: you pay a small fee per run start and per result row delivered to the dataset. See the Pricing tab for the current prices.

- Products mode is charged per product row, variants mode per variant row.
- With change tracking, `removed` rows count as results; with "Output only changed products" unchanged rows are not delivered and not charged.
- Rows describing stores that could not be read are free.
- Set **Maximum cost per run** in the run options to cap spending. The Actor stops cleanly when the cap is reached, keeps everything delivered so far, and notes it in `OUTPUT`.

### Use from an AI agent or via API

Ready-to-run Python, Node.js and cURL examples: [github.com/cedarfetch/shopify-store-scraper-examples](https://github.com/cedarfetch/shopify-store-scraper-examples).

Run synchronously and get the rows back in one HTTP call:

```bash
curl -X POST "https://api.apify.com/v2/acts/<username>~shopify-store-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"storeUrls":["allbirds.com"],"maxProductsPerStore":50,"outputMode":"variants"}'
```

JavaScript:

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('<username>/shopify-store-scraper').call({ storeUrls: ['allbirds.com'], trackChanges: true, changesOnly: true });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Python:

```python
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("<username>/shopify-store-scraper").call(run_input={"storeUrls": ["allbirds.com"]})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

The Actor is also available to agents through the Apify MCP server, which exposes Store Actors as tools. Tips for agents: keep `maxProductsPerStore` small for exploratory calls, use `outputMode: "variants"` when you need per-SKU prices, and read `recordType` to separate product rows from `store_status` rows.

### Limits and honest caveats

- **Public data only.** It reads what any shopper's browser can read without logging in. Unpublished products, wholesale/B2B catalogs, customer-specific prices and admin data are out of reach by design.
- **Password-protected stores are not supported** and are reported as `password_protected`.
- **Bot protection is not bypassed.** A few stores put Cloudflare, Vercel or similar challenges in front of their catalog. Those are reported as `blocked`.
- **Headless Shopify** stores (custom frontend on another platform, e.g. Hydrogen on Vercel) usually do not expose the catalog endpoints on their main domain and are reported as `not_shopify`. If you know the store's `*.myshopify.com` domain, try that.
- **Currency** is the store's base currency from `/meta.json`. Stores selling in several currencies through Shopify Markets show base-currency prices; `currency` is `null` if a store hides `/meta.json`.
- **Stock counts** (`inventoryQuantity`) appear only with `enrichVariants` and only when the store publishes them. Otherwise you get `available` true/false per variant.
- **Stores above 25,000 products** are read beyond that limit through the sitemap, one request per extra product, so they take longer.
- Shopify's `updated_at` changes on many edits (including stock), so `updatedAfter` is a "recently touched" filter, not "price changed". Use change tracking for exact changes.

### FAQ

**How do I find out if a site is a Shopify store?** Just add it. Non-Shopify sites are reported as `not_shopify` within a couple of requests and are not charged.

**Can I scrape several stores in one run?** Yes, up to 1,000 per run. Three are crawled at a time by default.

**How fast is it?** About one request per 250 products. A 1,000-product store takes a few seconds of network time.

**Why do I see fewer products than the store's homepage suggests?** Only published products visible to the public catalog are included; check the `OUTPUT` summary for warnings such as a capped run or a collection that returned nothing.

**Do I need proxies?** No. Shopify's public endpoints answer plain requests. Stores that block plain requests are reported, not worked around.

**Can I track only one collection?** Yes, paste the collection URL. Its snapshot is kept separately from the whole-store snapshot.

**Is this legal?** The Actor collects publicly available product information and no personal data. You are responsible for how you use the data and for respecting each site's terms. If in doubt, ask a lawyer.

### Support

Found a store that behaves oddly? Open an issue on the Issues tab with the store domain and the run ID; the `OUTPUT` record usually shows what happened.

### More Actors from Cedarfetch Labs

Built the same way: public data only, typed output, per-source error reporting, change tracking for scheduled runs.

- [ATS Jobs Scraper](https://apify.com/cedarfetch/ats-jobs-scraper): open jobs from Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee and Personio boards, with new and removed postings flagged.
- [Remote Jobs Scraper](https://apify.com/cedarfetch/remote-jobs-scraper): remote jobs from Remote OK, We Work Remotely and five more boards in one deduplicated feed.
- [Substack Scraper](https://apify.com/cedarfetch/substack-scraper): public posts from any Substack newsletter as Markdown, with a new-posts feed.
- [Google Trending Searches](https://apify.com/cedarfetch/google-trending-searches): what is trending on Google right now, per country, with alerts on new trends.

# Changelog

This Actor's version history is a separate document: https://apify.com/cedarfetch/shopify-store-scraper/changelog.md

# Actor input Schema

## `storeUrls` (type: `array`):

One store per line. Accepts bare domains (allbirds.com), full URLs (https://www.allbirds.com/), myshopify domains (store-name.myshopify.com), a collection URL (https://shop.com/collections/sale) to read only that collection, or a product URL to read only that product. Several entries for the same store are merged.

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

Stop after this many products per store. 0 means the whole catalog. Useful for sampling and for keeping test runs cheap.

## `outputMode` (type: `string`):

"products": one row per product, variants nested as a list (best for JSON and APIs). "variants": one row per variant with product fields repeated and options in columns (best for spreadsheets and price monitoring by SKU).

## `collectionHandles` (type: `array`):

Optional. Collection handles (the part after /collections/ in the URL, e.g. "sale" or "mens-shoes") to read instead of the whole catalog. Applied to every store in the list.

## `updatedAfter` (type: `string`):

Optional. Keep only products whose Shopify updated_at is on or after this moment. Accepts an ISO date (2026-11-01 or 2026-11-01T12:00:00Z) or a relative value such as "7 days" or "24 hours". Shopify bumps updated_at on most edits, including price and stock changes.

## `trackChanges` (type: `boolean`):

Compare every product with the previous run and add changeType (new, price_changed, back_in_stock, out_of_stock, unchanged, removed), the list of all changes, and the previous price and availability. Products that disappeared are reported as rows with changeType "removed". The comparison snapshot is kept in a named key-value store in your account, so it survives between runs. Schedule the Actor to get a daily change feed.

## `changesOnly` (type: `boolean`):

Requires change tracking. Skips rows whose changeType is "unchanged", so you only receive (and pay for) what changed. The snapshot is still refreshed.

## `snapshotStoreName` (type: `string`):

Name of the key-value store that holds the change-tracking snapshots. Use a different name to run independent tracking jobs on the same stores (for example one per client). Lowercase letters, digits and hyphens.

## `includeCollections` (type: `boolean`):

Add a "collections" field listing every collection (handle and title) each product belongs to. Costs extra requests: about one request per 250 products in each collection, so it is slower on stores with many collections.

## `maxCollectionsPerStore` (type: `integer`):

When collection membership is on, map at most this many collections per store (in the order Shopify lists them). 0 means all.

## `enrichVariants` (type: `boolean`):

Make one extra request per product to the public product endpoint to add variant barcodes (GTIN/UPC) and, where the store publishes it, inventoryQuantity. Roughly doubles run time on large stores.

## `concurrency` (type: `integer`):

How many requests run at once against a single store. Keep it low; Shopify rate-limits busy clients.

## `requestDelayMs` (type: `integer`):

Pause after each request to the same store, with small random jitter.

## `maxConcurrentStores` (type: `integer`):

How many different stores are crawled at the same time.

## `requestTimeoutSecs` (type: `integer`):

Give up on a single request after this many seconds (it is then retried).

## `maxRetries` (type: `integer`):

Retries for rate-limit (429), server-error (5xx) and network failures, with jittered exponential backoff that honours Retry-After. 401/403 and bot-protection pages are never retried.

## Actor input object example

```json
{
  "storeUrls": [
    "allbirds.com",
    "https://kith.com/collections/footwear",
    "store-name.myshopify.com"
  ],
  "maxProductsPerStore": 500,
  "outputMode": "products",
  "collectionHandles": [
    "sale",
    "new-arrivals"
  ],
  "updatedAfter": "7 days",
  "trackChanges": false,
  "changesOnly": false,
  "snapshotStoreName": "shopify-store-scraper-snapshots",
  "includeCollections": false,
  "maxCollectionsPerStore": 100,
  "enrichVariants": false,
  "concurrency": 2,
  "requestDelayMs": 300,
  "maxConcurrentStores": 3,
  "requestTimeoutSecs": 30,
  "maxRetries": 4
}
```

# Actor output Schema

## `products` (type: `string`):

All product or variant rows, plus store_status rows for stores that could not be read.

## `changes` (type: `string`):

The same rows shown with the change-tracking columns.

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

Per-store status (ok, partial, blocked, password_protected, not_shopify, unreachable...), counts, request totals and change counts.

# 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 = {
    "storeUrls": [
        "www.deathwishcoffee.com"
    ],
    "maxProductsPerStore": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("cedarfetch/shopify-store-scraper").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 = {
    "storeUrls": ["www.deathwishcoffee.com"],
    "maxProductsPerStore": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("cedarfetch/shopify-store-scraper").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 '{
  "storeUrls": [
    "www.deathwishcoffee.com"
  ],
  "maxProductsPerStore": 20
}' |
apify call cedarfetch/shopify-store-scraper --silent --output-dataset

```

## MCP server setup

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

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/KwvtsFb5DWEZB1RoV/builds/imsiXaQW0hF37Hth3/openapi.json
