# Ad Intelligence Watchlist — Facebook Ads, Changes, Ad Copy (`architekture/ad-intelligence-watchlist`) Actor

Meta Ad Library, any country (facebook.com/ads/library): new and changed Facebook and Instagram ads per advertiser since the last run: ad text, headline, link, CTA, format, start and end dates, platforms and changed fields. Input page IDs, Ad Library URLs or page names. Pay per result, no login.

- **URL**: https://apify.com/architekture/ad-intelligence-watchlist.md
- **Developed by:** [architek.earth](https://apify.com/architekture) (community)
- **Categories:** Social media, Lead generation, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 new or changed ads

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

## Ad Intelligence Watchlist — Facebook Ads, Changes, Ad Copy

**Recorded run: 79 ads checked from 1 advertiser in 22 seconds** (2026-10-22, build 0.0.5, run `5lzIhvfa6cudesULN`). A bounded canary sample, not a full-catalogue benchmark. No ad had changed since the previous run, so it delivered one free summary row and one checking unit.

architek.earth — an independent data studio. Pay-per-result scrapers with recorded runs, published canaries and stable schemas, built for people and AI agents.

Meta Ad Library competitor ads, watched by advertiser: give it Facebook page IDs, Ad Library URLs or exact page names, and each run returns the Facebook and Instagram ads that are new or changed since the last run, with ad text, headline, link, CTA, dates, platforms and the fields that changed.

| Source | Market | From | Standout fields |
|---|---|---|---|
| Meta Ad Library (facebook.com/ads/library) | Any country, or `ALL` | $1.50 / 1,000 ad rows | `changeType` / `changedFields`, `adText` / `linkTitle` / `linkUrl`, `startedAt` / `isActive` |

### ✅ Why choose this scraper

#### Track competitor Facebook ads daily, not the whole library

A snapshot per advertiser and country is saved in a named store in your Apify account (`ad-watchlist-<watchlistName>`). The first run delivers every ad in the window once (`isBaseline: true`); later runs deliver only rows with `changeType` `new` or `updated`.

#### See which ad copy, link or CTA changed

`updated` rows carry `changedFields`, compared across `adText`, `linkTitle`, `linkUrl`, `ctaText`, `isActive`, `endedAt`, `platforms`, `impressionsRange` and `spendRange`. `endedAt` stays null while an ad runs, so Meta's moving end date does not count as a change: 292 of 292 ads came back unchanged on the 2026-10-19 re-check, and 79 of 79 on the 2026-10-22 canary.

#### Watch an advertiser by page name without it drifting

An exact page name is resolved once to its page ID and pinned in the watchlist, so a schedule never switches pages. A name with no exact match gets a free `not_resolved` row listing the candidate pages.

#### Unchanged ads produce no ad rows

Unchanged, removed and aged-out ads are counted in the free summary row, not delivered. They still count toward the checking unit (see Pricing).

#### Measured on a schedule

A daily canary task replays bundled recorded ads through the full baseline and diff pipeline; a second canary walks Casper's live US Ad Library page and is the hero run above.

### ⚠️ Worth knowing up front

- The first run on a watchlist name and advertiser is a paid baseline: every ad in the `sinceDays` window is delivered once with `isBaseline: true`. A missing or corrupt snapshot triggers a labelled `BASELINE_REBUILT` that re-delivers and charges the window again; `rebuildReason` says why.
- Meta only. TikTok and other platforms get a free `not_supported` status row. There are no keyword searches: you watch named advertisers.
- `spendRange` and `impressionsRange` are filled only where Meta publishes them (political and issue ads); on commercial ads they are null. Ad image and video URLs are not returned.
- Meta refuses Ad Library pagination from datacenter IPs (tested 2026-10-19), so Apify Residential (US) is the default proxy and the only tested option.
- Each country is its own snapshot. An ad shown in the US appears under both `ALL` and `US` if you watch both, and is charged in each.
- When `maxAdsPerAdvertiser` is hit, or Meta interrupts a walk, the summary says `PARTIAL` and removals are not judged that run. A walk Meta refuses before any ad is seen ends `FAILED` in the summary, is not charged, and leaves the snapshot unchanged.
- One run at a time per watchlist name: a run that finds another run's active lease stops with `WATCHLIST_BUSY` and fetches nothing. The lock is best-effort, so do not start two runs on one name at once.
- A watchlist entry that is not a Facebook page ID, URL or name (`invalid_input`), a name with no exact match (`not_resolved`), a blocked name search (`blocked`), another platform (`not_supported`) or the same page listed twice (`duplicate`) gets a free status row (`"recordType": "status"`). Every advertiser and country that finishes gets a free summary row (`"recordType": "summary"`). Invalid run input, the spending limit, the run timeout and a busy watchlist write no status row; they end with the run's status message and `RUN_SUMMARY.status`.

### ⚡ Try the example

This actor has no free preview; the example below costs about $0.035 on its first run (20 ad rows plus one checking unit). For a free dry run, use `{"watchlist": ["sample:casper"]}`: bundled recorded ads, offline, never charged.

Open the actor's input page, switch to JSON, paste this and start the run:

```json
{
  "watchlist": ["308998183837"],
  "countries": ["US"],
  "sinceDays": 30,
  "watchlistName": "warby-parker",
  "maxAdsPerAdvertiser": 20
}
```

Remove `maxAdsPerAdvertiser` (default 1000) for the full window, then save the input as a task and schedule it; keep `watchlistName` fixed, because a new name starts a new baseline.

### ⚙️ Input

| Parameter | Type | Default | Description |
|---|---|---|---|
| `watchlist` | array | — | Facebook page IDs (`308998183837`), Ad Library or page URLs (`...?view_all_page_id=224110981099062`), or exact page names (`"Glossier"`). Vanity URLs (`facebook.com/warbyparker`) are resolved like names. Objects work too: `{"advertiser": "...", "platform": "meta"}`. Up to 50 entries; `sample:casper` runs a free offline dry run. |
| `sinceDays` | integer | `30` | Ads delivered at any time in the last N days are checked (1–90). Ads that stopped earlier age out, free. |
| `countries` | array | `["ALL"]` | Two-letter country codes or `ALL`, up to 5. Each one is its own snapshot and walk. |
| `watchlistName` | string | `default` | Names the persistent snapshot store: 1–40 letters, digits and inner hyphens, stored lowercase. |
| `maxAdsPerAdvertiser` | integer | `1000` | Cap on ads checked per advertiser and country (10–5000). When hit, the summary is `PARTIAL` and removals are not judged. |
| `proxyConfiguration` | proxy | Residential (US) | Proxy settings. Datacenter IPs are refused by Meta. |
| `feedback` | string | — | Optional note for the maintainers. It is logged and read by a human, and it never changes output. |

### 🧱 Stable fields

These top-level fields keep the same name and the same kind of value in every release. Fields are only ever added, never renamed or removed, so an n8n, Make, Zapier or spreadsheet pipeline can bind to them.

| Field | Type | Meaning |
|---|---|---|
| `adId` / `adLibraryUrl` | string | Meta's Ad Library ID and its public page. |
| `advertiser` / `pageId` | string | Advertiser page name and Facebook page ID. |
| `country` | string | The country scope the ad was checked in, or `ALL`. |
| `adText` / `linkTitle` / `linkUrl` / `ctaText` | string, nullable | Ad copy, headline, destination link and call to action. Dynamic-creative template placeholders (`product.name`) are replaced by the first card's copy. |
| `displayFormat` | string, nullable | Meta's format label, such as `VIDEO` or `DCO`. |
| `startedAt` / `endedAt` | string, nullable | ISO 8601 UTC. `endedAt` is set only once an ad has stopped. |
| `isActive` | boolean | Whether Meta lists the ad as active. |
| `platforms` | array | Placements such as `facebook`, `instagram`, `messenger`, `audience_network`, `threads`. |
| `impressionsRange` / `spendRange` | string, nullable | As Meta publishes them for political and issue ads; otherwise null. |
| `changeType` / `changedFields` | string / array | `new` or `updated`; for updates, the compared fields that changed. |
| `snapshotFirstSeen` / `snapshotLastSeen` | string | When this watchlist first and last saw the ad. |
| `isBaseline` | boolean | True on a first run or a `BASELINE_REBUILT`. |
| `collationId` | string, nullable | Meta's group ID for near-identical ads; each keeps its own `adId`. |
| `snapshotStatus` / `checked` / `new` / `updated` / `skippedUnchanged` / `removed` / `agedOut` / `snapshotUnitsCharged` / `status` | mixed | Summary rows, per advertiser and country. `snapshotStatus`: `BASELINE`, `DIFF` or `BASELINE_REBUILT`; `status`: `OK`, `PARTIAL`, `FAILED`, `BLOCKED` (Meta served a challenge page; free) or `VANISHED`. |
| `input` / `reason` / `candidates` | string / string / array | Status rows: the entry, why it was not checked, and candidate pages for `not_resolved`. |
| `recordType` | string | `"ad"` on ad rows, `"summary"` on free summary rows, `"status"` on free status rows. |

Other fields are extras, not part of the contract. To have one pinned, say so in `feedback`.

### 📦 Output example

Abridged real row from run `pslnacuL89Ozw3x3H` on 2026-10-22; values are a historical snapshot.

```json
{
  "recordType": "ad",
  "adId": "2248685125971563",
  "advertiser": "Casper",
  "pageId": "224110981099062",
  "platform": "meta",
  "country": "US",
  "adText": "Our most popular mattress just got an upgrade. ...",
  "linkTitle": "Cloud One - Premium Foam Mattress",
  "linkUrl": "https://casper.com/products/casper-cloud-one-foam",
  "ctaText": "Shop now",
  "displayFormat": "DCO",
  "startedAt": "2026-05-22T07:00:00Z",
  "endedAt": null,
  "isActive": true,
  "platforms": ["audience_network", "facebook", "instagram", "messenger", "threads"],
  "impressionsRange": null,
  "spendRange": null,
  "snapshotFirstSeen": "2026-10-22T10:52:24Z",
  "snapshotLastSeen": "2026-10-22T10:52:24Z",
  "changeType": "new",
  "changedFields": [],
  "isBaseline": true,
  "collationId": "917494687363191",
  "adLibraryUrl": "https://www.facebook.com/ads/library/?id=2248685125971563"
}
```

The free summary row from the hero run `5lzIhvfa6cudesULN`, a day with no changes:

```json
{
  "recordType": "summary",
  "advertiser": "Casper",
  "pageId": "224110981099062",
  "country": "US",
  "snapshotStatus": "DIFF",
  "walkComplete": true,
  "checked": 79,
  "delivered": 0,
  "new": 0,
  "updated": 0,
  "skippedUnchanged": 79,
  "removed": 0,
  "snapshotUnitsCharged": 1,
  "status": "OK"
}
```

A free status row looks like this (run `8z0cfRs0ZOIZlpqT6`):

```json
{
  "recordType": "status",
  "input": "https://www.instagram.com/casper/",
  "platform": "meta",
  "status": "invalid_input",
  "reason": "Not a Facebook page ID, page/Ad Library URL or advertiser name.",
  "candidates": []
}
```

### 🧠 Advanced usage

#### Get a daily digest of new competitor ads

Schedule the task each morning, add a webhook on run success, and pass rows with `changeType: "new"` to Slack, a sheet or an LLM summary.

#### Spot ad copy tests as they happen

Filter `changeType: "updated"` rows whose `changedFields` include `adText`, `linkTitle` or `ctaText`.

#### Compare one brand's ads across markets

`countries: ["US", "DE"]` keeps a separate snapshot and summary per country; pick either `ALL` or named countries for an advertiser, not both.

#### Check that a run finished

Read `RUN_SUMMARY`: `status` is `BASELINE`, `COMPLETE`, `BASELINE_REBUILT`, `PARTIAL`, `BUDGET_LIMIT`, `TIME_LIMIT`, `ABORTED`, `WATCHLIST_BUSY`, `STATE_SAVE_FAILED`, `STATE_READ_FAILED`, `PUSH_FAILED`, `INVALID_INPUT` or `FAILED` (hosted pricing differs from the approved events; nothing fetched or charged), next to `delivered`, `charged`, `rebuilt`, `pendingAdvertisers` and `perAdvertiser`.

### 🔌 Integrations

Results land in a dataset you can export as JSON, CSV or Excel. They can also flow to Google Sheets, Make, Zapier or n8n, or to a webhook when a run finishes. The Apify Scheduler runs it on a timetable. One run a day on a fixed `watchlistName` gives a daily change log per advertiser. Copy-paste code in Python, Node, curl and n8n: [architek-earth/examples/ad-intelligence-watchlist](https://github.com/architek-earth/examples/tree/main/ad-intelligence-watchlist).

### 💸 Pricing

Pay per result. Platform and proxy usage are included. Summary and status rows are free, and a walk that saw no ads is not charged. Each batch of ad rows is written into the watchlist snapshot and the run's `PROGRESS` record before it is pushed and charged, so neither a resumed run nor a later run delivers it again, and checking units are booked once per advertiser and country per run. If a push fails, the next run on that advertiser checks the dataset and re-delivers only the rows that never arrived.

| Event | Price | What it covers |
|---|---|---|
| `ad` | $1.50 / 1,000 ad rows | One delivered ad row: new, updated, initial baseline or rebuilt baseline. |
| `advertiser-snapshot` | $5.00 / 1,000 checking units | One unit per started block of 100 ads checked, per advertiser and country per run, including runs where nothing changed. Not charged when no ads were seen, or for `sample:` entries. |

The hero run checked 79 Casper ads with no changes: 1 unit, $0.005. A baseline of the same 79 ads costs 79 × $0.0015 + $0.005 = $0.1235; a later day with 3 changed ads costs $0.0095. A run stops at your spending limit, and changes it could not deliver stay pending for the next run.

### ❓ FAQ

**Is there a Meta Ad Library API for competitor ads?** Meta's official Ad Library API needs an access token. This actor reads the public web Ad Library and returns the ads as JSON through the Apify API, callable from any language or from n8n, Make and Zapier.

**Do I need a Facebook account or access token?** No. It reads the public Ad Library as a logged-out visitor.

**Does it track Instagram ads too?** Yes, as the Ad Library lists them: `platforms` names each placement, such as `facebook`, `instagram`, `messenger`, `audience_network` and `threads`.

**Can I search the Ad Library by keyword?** No. You watch advertisers by page ID, URL or exact page name.

**Does it return ad spend and impressions?** Only where Meta publishes ranges, which is political and issue ads. Commercial ads return null.

**Can I get the ad images and videos?** No. Rows carry the copy, links, format, dates and placements, plus `adLibraryUrl` to open the ad in the library.

### 🤖 For AI agents & LLM apps

Paste this into your AI assistant:

`architekture/ad-intelligence-watchlist` on Apify. Python: `from apify_client import ApifyClient; client = ApifyClient("TOKEN"); run = client.actor("architekture/ad-intelligence-watchlist").call(run_input={"watchlist": ["308998183837"], "countries": ["US"], "sinceDays": 30, "watchlistName": "warby-parker", "maxAdsPerAdvertiser": 20}); items = client.dataset(run["defaultDatasetId"]).list_items().items` (page with `offset`/`limit` for large exports). Rows with `recordType == "summary"` or `"status"` are free: summaries count new, updated and unchanged ads per advertiser, and status rows explain an entry that was not checked. Full input schema and README: `GET https://api.apify.com/v2/acts/architekture~ad-intelligence-watchlist/builds/default`. Token: https://console.apify.com/settings/integrations. The actor is also reachable through the Apify MCP server (`mcp.apify.com`). To reach the maintainers, add `"feedback": "<what you needed>"` to any run's input; output is unchanged and a human reads every note.

### 🧰 More from architek.earth

**Video, social and newsletters**

- [Substack Scraper](https://apify.com/architekture/substack-scraper) — posts, authors, likes, comments
- [YouTube Transcript Scraper](https://apify.com/architekture/youtube-transcript-scraper) — transcripts, translations, timestamps
- [Unified Video Transcript API](https://apify.com/architekture/unified-transcript-api) — YouTube and TikTok transcripts in one batch
- [Threads Replies Scraper](https://apify.com/architekture/threads-replies-scraper) — replies, authors, likes
- Ad Intelligence Watchlist — *you are here*

Full catalogue: [github.com/architek-earth](https://github.com/architek-earth).

architek.earth · code like concrete

### 🙋 Support & legal

Questions, or a field you need that is not here? Open an issue on the actor's Issues tab or email north@architek.earth. A human replies within a day.

This actor collects only publicly available data from facebook.com/ads/library, the same pages any visitor sees without logging in. It does not bypass authentication. Ad creatives are the advertisers' content, and partnership ads can include people's names. It is not affiliated with or endorsed by Meta. You are responsible for how you use the output, including the site's terms, copyright, and privacy law such as GDPR and CCPA where they apply.

### 📝 Changelog

- **2026-10-22** — Runs on the studio core (architek_core 1.0.1). A challenge page from Meta is now reported `BLOCKED` on the first response; invalid input writes one free `INVALID_INPUT` status row; a budget spent exactly by the last ad row ends normally instead of `BUDGET_LIMIT`. Fields and prices unchanged.
- **2026-10-22** — Status message now reads `Done: N rows, M details, K status rows (STATUS)`.
- **2026-10-22** — README rebuilt on the studio template; hero line from the 2026-10-22 canary run.
- **2026-10-19** — Clarified paid baselines and rebuilds, checking fees on unchanged runs, per-scope rounding and partial-walk charges. Added exact pricing examples and removed the unverified TikTok release promise. Prices and extraction code unchanged.
- **2026-10-19** — 1.0. Meta watchlist: persistent per-advertiser snapshots, new/updated diff with changed fields, baseline and BASELINE_REBUILT labelling, free summaries and status rows, name pinning, residential walk with IP rotation, resumable billing, offline canary sample.

# Actor input Schema

## `watchlist` (type: `array`):

Advertisers as Facebook page IDs ("224110981099062"), Ad Library or page URLs (…?view_all_page_id=…), or exact page names ("Warby Parker" — resolved once, then pinned). Objects work too: {"advertiser": "…", "platform": "meta"}. TikTok entries get a free not_supported row until v1.1. Up to 50.

## `sinceDays` (type: `integer`):

Ads delivered at any time in the last N days are checked. Ads that stopped before the window simply age out (free, counted).

## `countries` (type: `array`):

Two-letter codes (US, DE …) or ALL. Each country is its own snapshot and walk, so it multiplies the work. Up to 5.

## `watchlistName` (type: `string`):

Names the persistent snapshot store (ad-watchlist-<name>): 1-40 lowercase letters, digits and inner hyphens. Keep it fixed for a scheduled task; a new name starts a new baseline. Don't run two schedules on one name at the same time.

## `maxAdsPerAdvertiser` (type: `integer`):

Safety cap on the walk (and cost). When it is hit the advertiser is reported PARTIAL and removals are not judged that run.

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

Meta refuses Ad Library pagination from datacenter IPs (tested 2026-10-19). Apify Residential (US) is the default and the only tested option.

## `feedback` (type: `string`):

Tell the maintainers what you needed and did not get. Read by a human; does not change the output.

## Actor input object example

```json
{
  "watchlist": [
    "Warby Parker",
    "https://www.facebook.com/ads/library/?view_all_page_id=224110981099062"
  ],
  "sinceDays": 30,
  "countries": [
    "ALL"
  ],
  "watchlistName": "default",
  "maxAdsPerAdvertiser": 1000,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

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

recordType ad rows (charged) plus free summary/status rows.

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

RUN_SUMMARY: per advertiser BASELINE / DIFF / BASELINE_REBUILT, counts and charges.

# 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 = {
    "watchlist": [
        "Warby Parker",
        "https://www.facebook.com/ads/library/?view_all_page_id=224110981099062"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("architekture/ad-intelligence-watchlist").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 = {
    "watchlist": [
        "Warby Parker",
        "https://www.facebook.com/ads/library/?view_all_page_id=224110981099062",
    ],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("architekture/ad-intelligence-watchlist").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 '{
  "watchlist": [
    "Warby Parker",
    "https://www.facebook.com/ads/library/?view_all_page_id=224110981099062"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call architekture/ad-intelligence-watchlist --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,architekture/ad-intelligence-watchlist"
        }
    }
}
```

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/uGqpK4od0nG11k2GH/builds/i5qh0GUboWLuu0aYi/openapi.json
