# ScrollStash for agents ScrollStash tracks competitors' Meta (Facebook and Instagram) ads and reports what changed: ads started and stopped, landing-page changes, launches, sales, creator and whitelisting pages, and how long ads have been observed running. - [ScrollStash MCP](/docs/mcp.md): Connect Claude Code, Claude.ai, ChatGPT, Codex or Cursor to ScrollStash MCP. - [REST API](/docs/api.md): Call the ScrollStash API directly with an ss_live_ API key. - [Catalog](/docs/catalog.md): Every REST operation, generated from the contract: endpoint, scope and description. - [Limits](/docs/limits.md): What the data is and is not. Read before quoting any figure. - [OpenAPI](https://api.scrollstash.com/v1/openapi.json): the machine-readable REST contract. Meta Ad Library data only (Facebook and Instagram ads), US ecommerce focus. No spend, reach, impressions or ROAS: how long an ad runs is observed survival, not performance. Every response carries `as_of`. --- # ScrollStash MCP A remote MCP server (streamable HTTP) with OAuth sign-in. Your client opens a browser to sign in to ScrollStash the first time. **Server URL:** `https://mcp.scrollstash.com/mcp` ## Claude Code ```bash claude mcp add --transport http scrollstash https://mcp.scrollstash.com/mcp ``` Then run `/mcp` in Claude Code and choose **Authenticate** for `scrollstash`. ## Claude.ai and Claude Desktop Settings → Connectors → **Add custom connector**. Name it `ScrollStash`, set the URL to `https://mcp.scrollstash.com/mcp`, then **Connect** and sign in. ## ChatGPT Custom MCP connectors need a plan and workspace setting that allow them (developer mode). Add a connector with the server URL `https://mcp.scrollstash.com/mcp` and OAuth authentication, then sign in. ## Codex ```bash codex mcp add scrollstash --url https://mcp.scrollstash.com/mcp ``` ## Cursor `~/.cursor/mcp.json`: ```json { "mcpServers": { "scrollstash": { "url": "https://mcp.scrollstash.com/mcp" } } } ``` ## Any other client ```json { "mcpServers": { "scrollstash": { "type": "http", "url": "https://mcp.scrollstash.com/mcp" } } } ``` ## Tools Ask your client to list the server's tools; each describes when to use it and what it returns. ## Limits Meta Ad Library data only (Facebook and Instagram ads), US ecommerce focus. No spend, reach, impressions or ROAS: how long an ad runs is observed survival, not performance. Every response carries `as_of`. Details: [/docs/limits.md](/docs/limits.md). --- # REST API **Base URL:** `https://api.scrollstash.com` · **OpenAPI:** [`https://api.scrollstash.com/v1/openapi.json`](https://api.scrollstash.com/v1/openapi.json) · **Catalog:** [/docs/catalog.md](/docs/catalog.md) ## Authentication Send an API key as a bearer token: ```bash curl -s https://api.scrollstash.com/v1/me -H "Authorization: Bearer $SCROLLSTASH_API_KEY" ``` - Keys start with `ss_live_`, belong to one workspace and carry scopes. The plaintext is shown once, at creation. - Self-service key creation is not open yet. - Without a valid key the API answers `401`. ## Responses Success is always `{ "data": …, "meta": { "request_id", "as_of", "cursor"?, "count"?, "lag_hours"? } }`. Errors are always `{ "error": { "type", "message", "status", "request_id", "details"? } }`: | type | status | meaning | | --- | --- | --- | | `bad_request` | 400 | input rejected; `details` has the issues | | `unauthorized` | 401 | missing or invalid key | | `payment_required` | 402 | plan quota used up (e.g. tracked-brand slots) | | `forbidden` | 403 | key or plan lacks the scope | | `not_found` | 404 | no such resource | | `conflict` | 409 | already exists or changed | | `rate_limited` | 429 | wait for `x-ratelimit-reset` | | `internal` | 500 | retry; report the `request_id` | ## Workspaces Paths with `{ws}` act on a workspace: an API key's own workspace id (see `GET /v1/me` → `data.workspace_id`). ## Pagination List operations take `limit` (1–100, default 20) and `cursor`. Pass `meta.cursor` back until it is absent. ## Limits Meta Ad Library data only (Facebook and Instagram ads), US ecommerce focus. No spend, reach, impressions or ROAS: how long an ad runs is observed survival, not performance. Every response carries `as_of`. Details: [/docs/limits.md](/docs/limits.md). --- # Catalog Every operation of the ScrollStash API, generated from the API contract: 85 operations. Each operation has one REST endpoint. REST paths are relative to `https://api.scrollstash.com`. Paths with `{ws}` act on a workspace: your API key's own workspace id (`GET /v1/me` → `data.workspace_id`). Paginated operations take `limit` (max 100) and `cursor`; the next cursor is `meta.cursor`. ## system | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/system/freshness` | `system:read` | Corpus size, last scrape time and per-category coverage. | ## brands | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/brands/{brandId}` | `brands:read` | One brand (by id or slug) with its current ad statistics. | | `GET /v1/brands` (paginated) | `brands:read` | Public brands, filterable by category, country and name, sorted by activity. | | `GET /v1/brands/sitemap` (paginated) | `brands:read` | Indexable brand pages for sitemap generation (slug, url, last update), optionally within a category at any level. | | `GET /v1/brands/{brandId}/velocity` | `brands:read` | Weekly ad counts for one brand: every ISO week with an observed day. active_ads is the latest observed day's value; new_ads and retired_ads sum reliable days only (reliable_days, 0–7, says how many) — shade weeks with reliable_days 0. | | `GET /v1/brands/{brandId}/breakdown` | `brands:read` | Active ads by creative format for one brand. | | `GET /v1/brands/{brandId}/similar` | `brands:read` | Sites Similarweb rates as similar to this brand, by affinity, linked to tracked brands. | | `GET /v1/brands/{brandId}/trajectory` | `brands:read` | Similarweb global / category rank and monthly visits over time, oldest first. | | `GET /v1/brands/{brandId}/demographics` | `brands:read` | Similarweb audience gender and age shares for the brand's domain. | | `GET /v1/brands/{brandId}/tech-stack` | `brands:read` | Platform tools detected on the brand's site, by tool category then name. | | `POST /v1/workspaces/{ws}/tracked-brands` | `tracking:write` | Track a brand in a workspace; the scraper hand-off happens asynchronously. | | `GET /v1/workspaces/{ws}/tracked-brands` (paginated) | `brands:read` | The workspace's tracked brands, newest first, each with its current public summary. | | `GET /v1/workspaces/{ws}/tracked-brands/freshness` | `brands:read` | Freshness SLO status of the workspace's tracked brands, most overdue first: the last reliable scrape session, when the next is due, and whether the cadence's SLO is breached. | | `DELETE /v1/workspaces/{ws}/tracked-brands/{brandId}` | `tracking:write` | Stop tracking a brand in a workspace; answers removed:false when it was not tracked. | | `GET /v1/brands/{brandId}/facebook-pages` | `brands:read` | Facebook pages linked to the brand with their servable role (quarantined links dropped, unservable claims as UNVERIFIED), followers and ad counts, most ads first. | | `GET /v1/brands/{brandId}/leakage` | `brands:read` | How many of the brand's active ads run on pages it does not own: totals over every page, and the ten largest third-party pages. | | `GET /v1/brands/{brandId}/copy-reuse` | `brands:read` | Ad body texts the brand runs that pages outside its linked set also run — at most twelve, by foreign ads. | | `GET /v1/brands/{brandId}/partnerships` | `brands:read` | Creators the brand ran branded content with in the last 365 days, both directions, merged per creator, most ads first. | | `GET /v1/brands/{brandId}/creative-library` | `brands:read` | A sample of the brand's active ads with stored media (half the longest-running, half the newest; at most `sample`, answered as sample_cap), plus format, CTA and longevity totals over every active ad. | | `GET /v1/brand-thumbs` | `brands:read` | Up to four of each brand's newest live creatives with a stored image or poster. Every brand asked is answered; an empty list means none is stored. | | `GET /v1/brands/{brandId}/daily-activity` | `brands:read` | Daily active, new and retired ads for one brand, oldest first. Unreliable days carry reliable:false (draw a gap); days never observed are absent, not zero. | | `GET /v1/brands/{brandId}/split-tests` | `brands:read` | The testing tools installed on the brand's site and every test a collector read on their accounts: type, state and its basis, arms with configured allocations, the pages each acts on with the brand's active ads there, and the category's test-type profile. Configured and observed: it never says which arm did better. No price or shipping amount is served — a price test says it is one. | | `GET /v1/brands/{brandId}/store` | `brands:read` | The brand's public Shopify catalog as the collector read it: the latest scan's contract (only an exhausted enumeration is the whole feed), products with variants, listed prices and the available flag as published, and twelve weeks of observed changes. A brand never scanned answers status not_collected, never an empty catalog. | | `GET /v1/brand-network` | `brands:read` | The shared-page network of one brand (it and its direct neighbours) or of one category (its public brands): brands, brand-to-brand links with their confidence and shared page count, and how many brands in a category have no link. Ownership claims are not part of it. | ## categories | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/categories/{categoryId}` | `categories:read` | One category (any level) with its parent and current totals. | | `GET /v1/categories` (paginated) | `categories:read` | Top-level categories, or the children of one parent; with `slug`, the categories of any level carrying that slug (the site resolves `/categories/…` paths with it). | | `GET /v1/categories/sitemap` (paginated) | `categories:read` | Public category pages (every level) for sitemap generation. | | `GET /v1/categories/{categoryId}/stats` | `categories:read` | Aggregates for one category: brands, landing URLs, Similarweb visits, top page types and top tools. | ## rankings | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/rankings` (paginated) | `rankings:read` | Brand leaderboard for the brands whose top traffic country is `country`, filterable by category. | | `GET /v1/rankings/risers` (paginated) | `rankings:read` | Brands whose active ads grew most between a reliable day in the last week and one 7–28 days earlier. | | `GET /v1/rankings/new-advertisers` (paginated) | `rankings:read` | Brands tracked before the window whose first attributed ad (Meta's start date) started in the last `days` days, most active first. Read model rewritten nightly. | ## ads | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/ads` (paginated) | `ads:read` | Search ads by copy, format, status and country. Without `brand_id` the scan is bounded to ads started in the last 90 days. | | `GET /v1/ads/{adId}` | `ads:read` | One ad with its full copy, archived media and observation window. | | `GET /v1/brands/{brandId}/ads` (paginated) | `ads:read` | A brand's attributed ads (brand_ad_map), newest start first. | ## creatives | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/creatives` (paginated) | `creatives:read` | Archived creative assets for an ad, or for a brand's attributed ads. | | `GET /v1/creatives/{creativeId}` | `creatives:read` | One archived creative asset. | | `GET /v1/ads/{adId}/creatives` | `creatives:read` | Every archived creative asset of one ad. | | `GET /v1/creative-records` (paginated) | `creatives:read` | Search creative records (one per stored file, not per ad) by brand, analysis depth, media kind, any of the values of each closed field, words said or written on screen, and numeric ranges; most live ads first. Counts are ads carrying a file, never spend. | | `GET /v1/creative-records/{creativeKey}` | `creatives:read` | One creative record by its key or a prefix of at least ten characters: media facts with frame times, the transcript with corrections, the classification, and the corpus join (ads carrying it, live ads, first start, landing page, creator-run). An ambiguous prefix is a conflict. | | `GET /v1/creative-coverage` | `creatives:read` | How far the pipeline has taken a brand's creative files (or every public brand's), against its measured file count, and each closed field's values over the described files — the denominators every creative share is printed with. | ## pages | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/pages` (paginated) | `pages:read` | Landing pages of public US ecommerce brands on hosts they own — newest first, or most-seen first with `sort=popular`; filter by page type, category or a URL substring. With `brand_id` or `category_id`, every live page of that public brand, or of the brands in that category at any level — any country. | | `GET /v1/pages/{pageId}` | `pages:read` | One landing page with its scraped metadata and classification. | | `GET /v1/page-details` | `pages:read` | Up to 100 landing pages with their scraped metadata and classification, by id. Unknown or non-public ids are left out. | ## operators | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/operators` (paginated) | `operators:read` | Operators: companies owning several domains or pages. | | `GET /v1/operators/{operatorId}` | `operators:read` | One operator with the public brands it owns. | | `GET /v1/hubs` (paginated) | `operators:read` | Facebook pages running paid ads for two or more brands — affiliate hubs, commerce publishers, creator pages — with each brand roster and live creative counts, biggest roster first. | | `GET /v1/brands/{brandId}/operators` | `operators:read` | The operators that own this brand's domain, each with its other public brands. | ## tools | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/tools` (paginated) | `tools:read` | Platform tools detected on public brands' sites, most adopted first. | | `GET /v1/tools/{toolSlug}` | `tools:read` | One tool (by slug or uuid) with adoption by category, common combinations and a traffic split. | | `GET /v1/tools/{toolSlug}/brands` (paginated) | `tools:read` | Public brands using a tool, by Similarweb monthly visits; `country` keeps the brands whose top traffic country it is. | | `GET /v1/split-test-census` | `tools:read` | Every A/B-testing tool: public brands with it installed, how many of those a collector has read, how many run at least one experiment, and whether we read the tool at all (null counts mean not read, never zero). Personalizations and rollouts are not counted as tests. | ## audience | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/audience/{segment}` (paginated) | `audience:read` | Public brands whose Similarweb audience falls in a segment, by monthly visits, with their demographics. | ## me | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/me` | `me:read` | The caller's workspace, role, plan and granted scopes. | ## apiKeys | REST | Scope | What it does | | --- | --- | --- | | `POST /v1/workspaces/{ws}/api-keys` | `apiKeys:write` | Create an API key for a workspace. The plaintext key is in this response only; scopes must be a subset of the caller's. | | `GET /v1/workspaces/{ws}/api-keys` (paginated) | `apiKeys:read` | The workspace's API keys, newest first, revoked ones included; never the key itself. | | `POST /v1/workspaces/{ws}/api-keys/{keyId}/rotate` | `apiKeys:write` | Revoke a key and issue its replacement with the same name and scopes. The new plaintext is in this response only. | | `DELETE /v1/workspaces/{ws}/api-keys/{keyId}` | `apiKeys:write` | Revoke a key. Requests with it fail with 401 from the next request on. | | `POST /v1/workspaces/{ws}/api-keys/revoke-unused` | `apiKeys:write` | Revoke every active key not used in the given number of days (never-used keys count from creation). | ## radar | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/workspaces/{ws}/radar` (paginated) | `tracking:read` | Observed ad changes across the workspace's tracked brands in the last `days` days, newest first: ads started, ads the scraper confirmed inactive, and ads whose destination link changed. | | `GET /v1/workspaces/{ws}/radar/signals` (paginated) | `tracking:read` | Grouped radar signals for the workspace's tracked brands in the last `days` days, newest first: ads started, confirmed stops and link changes per brand and day, plus activity changes (active-ad ramps and drops, launch bursts, sales, new landing pages, new linked pages, new whitelisting pairs, format-mix shifts, long-running ads refreshed). Each carries evidence links. | ## alerts | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/workspaces/{ws}/alerts` (paginated) | `alerts:read` | The workspace's alert rules, newest first. | | `POST /v1/workspaces/{ws}/alerts` | `alerts:write` | Create an alert rule on brands the workspace tracks: a trigger (new ads, confirmed stopped ads, page changes, new partners or activity coverage), a delivery cadence, and its channels (email to the creator, optional https webhook or Slack incoming webhook). The first webhook rule answers the workspace's webhook signing secret once. | | `PATCH /v1/workspaces/{ws}/alerts/{alertId}` | `alerts:write` | Change an alert rule's name, trigger, brands, cadence, channels or whether it is enabled; webhook_url null removes the webhook. | | `DELETE /v1/workspaces/{ws}/alerts/{alertId}` | `alerts:write` | Delete an alert rule and its delivery history; answers removed:false when it did not exist. | | `GET /v1/workspaces/{ws}/alert-deliveries` (paginated) | `alerts:read` | The workspace's alert deliveries, newest first, optionally for one rule. | | `POST /v1/workspaces/{ws}/alerts/{alertId}/test` | `alerts:write` | Send a sample delivery for an alert rule on each of its channels now and answer what happened. Webhooks are POSTed JSON signed with the workspace secret: header x-scrollstash-signature is v1=hex(HMAC-SHA256(secret, `${x-scrollstash-timestamp}.${raw body}`)). | | `POST /v1/workspaces/{ws}/alerts/webhook-secret` | `alerts:write` | Create or rotate the workspace's alert webhook signing secret and answer it once; it is never returned again and the previous secret stops signing immediately. | ## briefs | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/workspaces/{ws}/briefs` (paginated) | `briefs:read` | The workspace's weekly briefs, newest period first: each period's radar change counts across its tracked brands. | | `GET /v1/workspaces/{ws}/briefs/{briefId}` | `briefs:read` | One weekly brief: change counts per tracked brand for the period and up to ten notable radar items. | | `PATCH /v1/workspaces/{ws}/briefs/{briefId}` | `briefs:write` | Record whether a brief was useful, already known or wrong; null clears it. | ## reports | REST | Scope | What it does | | --- | --- | --- | | `POST /v1/workspaces/{ws}/reports` | `reports:write` | Queue a CSV export of the radar (ads started, ads stopped, link changes) for tracked brands over up to 31 days. Returns the queued report at once; an `idempotency_key` reused in the workspace returns the report it created. | | `GET /v1/workspaces/{ws}/reports` (paginated) | `reports:read` | The workspace's reports, newest first, each with its status and download URL once ready. | | `GET /v1/workspaces/{ws}/reports/{reportId}` | `reports:read` | One report with its status; `download_url` is set once it is ready. | ## collections | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/workspaces/{ws}/collections` (paginated) | `collections:read` | The workspace's collections with their items, most recently changed first. Private boards appear only to their creator and editors. | | `POST /v1/workspaces/{ws}/collections` | `collections:write` | Create a collection, optionally inside a folder and with a first item. | | `PATCH /v1/workspaces/{ws}/collections/{collectionId}` | `collections:write` | Rename, describe, move, or change who can see and edit a collection. | | `DELETE /v1/workspaces/{ws}/collections/{collectionId}` | `collections:write` | Delete a collection and every collection inside it. | | `POST /v1/workspaces/{ws}/collections/{collectionId}/items` | `collections:write` | Add an item to a collection; adding one already there changes nothing. | | `DELETE /v1/workspaces/{ws}/collections/{collectionId}/items/{itemType}/{itemKey}` | `collections:write` | Remove an item from a collection; answers removed:false when it was not there. | | `GET /v1/workspaces/{ws}/saved` (paginated) | `collections:read` | The workspace's saved items, newest first, optionally of one type. | | `POST /v1/workspaces/{ws}/saved` | `collections:write` | Save an item; saving one already saved changes nothing. | | `DELETE /v1/workspaces/{ws}/saved/{itemType}/{itemKey}` | `collections:write` | Unsave an item; answers removed:false when it was not saved. | ## notes | REST | Scope | What it does | | --- | --- | --- | | `GET /v1/workspaces/{ws}/notes` (paginated) | `notes:read` | The workspace's notes, most recently edited first, optionally on one subject type or one subject. | | `POST /v1/workspaces/{ws}/notes` | `notes:write` | Write the caller's note on a subject: creates it, or replaces the caller's existing note on that subject. | | `DELETE /v1/workspaces/{ws}/notes/{noteId}` | `notes:write` | Delete a note; answers removed:false when it did not exist. | --- # Limits - **Source.** The Meta Ad Library (Facebook and Instagram ads) only. No Google, TikTok or other networks. - **Coverage.** Brands ScrollStash tracks, with a US ecommerce focus. A brand missing from ScrollStash is not evidence it runs no ads. - **No money figures.** There is no spend, budget, reach, impressions, CPM, conversions or ROAS, and nothing may be inferred about them. - **Observed survival.** `days_active` and "long-running" describe how long an ad was *observed* running. A long-running ad may still lose money. Never call it "winning" or "best-performing". - **Freshness.** Every response has `meta.as_of`, and brand rows carry their own `as_of`. Quote it with every figure. - **Signals are observations.** Radar items say what changed (an ad started, stopped or changed destination; a new landing page). They don't say why. A drop in active ads is not a confirmed stop. - **Classification.** Page types, CTAs, audiences and page roles are automated classification and can be wrong. - **Partner and shadow pages.** A page running a brand's ads is an observed association, not proof of ownership or a contract. - **Rate limits.** 100 requests/min on the free plan and 1,000 on paid plans, per caller (`x-ratelimit-*` headers).