# Meta Ad Library Checker: Is a Brand Running Facebook Ads? (`accountable_eel/meta-ads-presence-lookup`) Actor

Check if a company is running Facebook or Instagram ads: give it a list of Facebook Pages, Page IDs or domains and get one row per brand from Meta's official Ad Library API. A Clay enrichment column. Full coverage for EU/UK-delivered ads. Bring your Meta token. Never charged for a miss.

- **URL**: https://apify.com/accountable\_eel/meta-ads-presence-lookup.md
- **Developed by:** [Adrian Voss](https://apify.com/accountable_eel) (community)
- **Categories:** Lead generation, Marketing
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 page found running ads

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Meta Ad Library Presence Lookup: Facebook Ads Library Checker

Check whether a Facebook Page is running ads, using **only Meta's official Ad Library API**
(`graph.facebook.com/ads_archive`) — no scraping, no proxy fights, no reverse-engineered
GraphQL that breaks the moment Meta rotates it. You bring your own Meta access token; this
actor never ships or stores one. A miss never costs you anything.

### Coverage — read this before you buy

Meta's official API does **not** return the same worldwide, every-ad coverage the public
Ad Library website's own scraper-fed clones claim. It returns:

- **Full ad coverage** — any category, any advertiser — for ads delivered **in the EU or UK**.
- **Political and issue ads only**, worldwide, for everything outside the EU/UK.

An ordinary commercial ad running only in, say, the US or Brazil will **not** show up here.
This is a genuine Meta platform restriction, not a limitation of this actor — see Meta's own
[Ad Library API reference](https://www.facebook.com/ads/library/api/). If your use case is
"does this US-only retailer run any ads at all," this actor will honestly report no result,
not a fabricated one.

### Features

- **Official API only.** Every request goes straight to `graph.facebook.com/ads_archive`
  with your own access token — nothing here scrapes the Ad Library website.
- **Page ID, Page name, or domain input.** A numeric Page ID is the most reliable match; a
  domain is matched as a best-effort text search (Meta has no domain-to-Page lookup), and is
  reported as such when it misses.
- **Presence check, or one row per ad.** By default one row per brand from up to 50 active
  ads — enough to answer "is this page actively advertising, and since when." Tick
  **Also return one row per ad** for each ad's Ad Library ID and link, running-since date,
  platforms, body text, headline, link caption and description, and languages (up to 500 ads
  per brand).
- **Competitor ad alerts.** Schedule a watchlist and get only the ads that are new since the
  last run, plus the ads that stopped — see [Competitor ad alerts](#competitor-ad-alerts).
- **Rate-limit aware.** Meta caps the API at roughly 200 calls/hour per token, and every 50 ads
  read is one call. Max concurrency
  defaults to 1, and a rate-limit response comes back as a clear, actionable row instead of a
  silent miss.
- **Never charged for a miss.** A page with no ads, an invalid token, or a search that finds
  nothing costs nothing — see [Pricing](#pricing).

### Competitor ad alerts

Looking for a Facebook Ad Library scraper that tells you when a competitor launches new ads?
This actor does it through Meta's official API instead of scraping, so it doesn't break when
the Ad Library website changes.

1. Put your competitors' Page IDs (most reliable) or Page names in the list, add your Meta token.
2. Tick **New ads alert: return only ads not seen before**. That turns on monitoring and ad rows.
3. Save it as a Task and add a daily or weekly Schedule.

The first run returns every active ad it reads as the starting list. Each later run returns
one row per ad that this watchlist has never seen, flagged `isNewAd: true`, and each brand's
summary row gets `newAdCount` plus `stoppedAds`: the IDs of ads that were active last run and
aren't now. Open any ID at `https://www.facebook.com/ads/library/?id=<ID>`.

A few rules keep the new ads alert honest:

- `stoppedAds` is only filled when both runs read the brand's **whole** active-ad list. If the
  list was cut off at **Max ads per brand**, or a Meta request failed half way, it stays
  `null` rather than guessing. Set the cap above the brand's ad count for stop alerts.
- The first run of a watchlist never reports stops (there's nothing to compare against).
- A watched brand whose ads all stopped still gets its summary row, with `activeAdCount: 0`.
- Name the watchlist (**Watchlist name**) to keep separate memories for separate schedules.
  The memory lives in a named key-value store on your own Apify account.

To send the new ads to Google Sheets, add Apify's Google Sheets integration to the Task (or an
n8n / Make step on the run's dataset) and filter on `rowType = "ad"`. Each run then appends
only the new ads to Google Sheets.

### How to use Meta Ad Library Presence Lookup — Official API

1. **In the Apify Console.** Open the actor page and click **Start** — the `pageQueries` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~meta-ads-presence-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"pageQueries":["nike.com","156800974367001"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

Don't have a Meta access token yet? Leave that field blank and run it anyway: every row comes back with status `NEEDS_TOKEN` so you can see the output shape before you apply for one, and it only costs the start fee.

**Bulk example.** A 27-Page watchlist of well-known advertisers, run with a real access token:

```json
{
  "pageQueries": [
    "nike.com", "adidas.com", "target.com", "walmart.com", "bestbuy.com",
    "starbucks.com", "mcdonalds.com", "coca-cola.com", "pepsi.com", "nestle.com",
    "airbnb.com", "booking.com", "expedia.com", "uber.com", "doordash.com",
    "spotify.com", "netflix.com", "hulu.com", "samsung.com", "sony.com",
    "hm.com", "zara.com", "sephora.com", "ulta.com", "chase.com",
    "americanexpress.com", "visa.com"
  ]
}
```

27 Pages/domains ≈ **$0.081** if every one is found ($0.003 per found row at the FREE tier, plus
the $0.00005 actor-start charge) — less for any that come back `BLOCKED`, `NOT_FOUND`, or
`NEEDS_TOKEN`, since misses are never billed.

### Input

```json
{
  "pageQueries": [
    "nike.com",
    "156800974367001"
  ]
}
```

One per line. A numeric Meta Ad Library Page ID, a Facebook Page name, or a company domain (domains are matched as best-effort text search, not a guaranteed domain-to-page mapping). Accepted formats: 156800974367001 (numeric Page ID — most reliable), Nike (Page name), nike.com (domain — best-effort).

Fill in **Your Meta Ad Library API access token** to get real results, apply for one at
[developers.facebook.com/docs/graph-api/reference/ads\_archive](https://developers.facebook.com/docs/graph-api/reference/ads_archive).
The field is marked secret: masked in the Console and encrypted at rest. It's optional: leave
it blank to get `NEEDS_TOKEN` rows instead of real data.

### Output

| query | found | status | pageId | rowType | pageName | activeAdCount | earliestActiveSince | platforms | sampleAdLibraryUrls | euReach | adListComplete | newAdCount | stoppedAdCount | stoppedAds | adArchiveId | adLibraryUrl | adStartedAt | adStopsAt | adCreatedAt | adPlatforms | adBodyText | adTitle | adLinkCaption | adLinkDescription | adVersionCount | adLanguages | adEuReach | isNewAd | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| nike.com | false | BLOCKED | <page id> | \<row type (brand = summary row, ad = one ad)> | <page name> | \<active ads found (this lookup: up to 50, or up to max ads per brand with ad rows or monitoring on)> | <earliest start date among ads found> | \<platforms (facebook / instagram / audience network / messenger)> | \<sample ad library links (public, no token)> | \<eu total reach (eu-targeted political/issue ads only)> | \<whole active-ad list read (false = cut off at the cap)> | \<new ads since the last run (monitoring)> | \<ads stopped since the last run (monitoring)> | \<stopped ad ids since the last run (monitoring)> | \<ad library id (ad rows)> | \<ad library link (ad rows)> | \<ad running since (ad rows)> | \<ad scheduled end, if set (ad rows)> | \<ad created (ad rows)> | \<ad platforms (ad rows)> | \<ad body text (ad rows)> | \<ad link title / headline (ad rows)> | \<ad link caption, usually the display domain (ad rows)> | \<ad link description (ad rows)> | \<creative versions in this ad (ad rows)> | \<ad languages (ad rows)> | \<ad eu total reach, where meta shows it (ad rows)> | \<new since the last run (ad rows, monitoring)> | 2026-09-22T06:10:15.361Z |

```json
{
  "query": "nike.com",
  "found": true,
  "status": "OK",
  "pageId": "15087023444",
  "pageName": "Nike",
  "activeAdCount": 50,
  "earliestActiveSince": "2026-05-12T00:00:00+0000",
  "platforms": ["facebook", "instagram"],
  "sampleAdLibraryUrls": ["https://www.facebook.com/ads/library/?id=..."],
  "euReach": null,
  "scrapedAt": "2026-08-24T00:00:00.000Z"
}
```

`activeAdCount` is capped at 50 per lookup (or at **Max ads per brand** once ad rows or
monitoring are on) — a page running more shows the cap, not its true total, and
`adListComplete` is `false`. `euReach` is Meta's EU ad-transparency reach figure, present only on EU-targeted
political/issue ads; `null` (not `0`) when it doesn't apply.

With **Also return one row per ad** on, each brand's ads come as one row per ad next to its
brand row (`rowType` tells them apart):

```json
{
  "query": "15087023444",
  "found": true,
  "status": "OK",
  "rowType": "ad",
  "pageId": "15087023444",
  "pageName": "Nike",
  "adArchiveId": "1202",
  "adLibraryUrl": "https://www.facebook.com/ads/library/?id=1202",
  "adStartedAt": "2026-09-12",
  "adStopsAt": null,
  "adPlatforms": ["instagram"],
  "adBodyText": "Made for the long run.",
  "adTitle": "Run your best",
  "adLinkCaption": "nike.com",
  "adLinkDescription": null,
  "adVersionCount": 2,
  "adLanguages": ["en"],
  "adEuReach": null,
  "isNewAd": true,
  "scrapedAt": "2026-09-25T06:00:00.000Z"
}
```

Every link in the output is the public Ad Library page, which needs no token. Meta's own
`ad_snapshot_url` embeds your access token, so this actor never writes it to the dataset.

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~meta-ads-presence-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"pageQueries":["nike.com","156800974367001"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~meta-ads-presence-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"pageQueries":["nike.com","156800974367001"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~meta-ads-presence-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"pageQueries":["{{Page or domain}}"]}`, mapping the row's Page or domain into the `pageQueries` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "Meta Ad Library Checker: Facebook & Instagram Ads" — the agent will find and run this actor.

### Pricing

- **Page found running ads**: $3 per 1,000 Pages or domains
- **Ad row returned**: $4 per 1,000 Pages or domains

Plus a $0.00005 start fee per run. Each event above is billed independently, only when it actually returns data — misses (`found:false`) are never charged.

Ad rows are billed per ad as a separate `ad` event, only when you tick **Also return one row
per ad** or **New ads alert** (see the Pricing tab). **Max ads per brand** caps them per brand.

### FAQ

**Why are there no image, video or landing-page URL columns?**
Meta's official Ad Library API doesn't return them. It returns the ad text, headline, link
caption (usually the display domain) and description, plus a rendered-ad link that only works
with your token. The `adLibraryUrl` link shows the full creative, images and video included.

**Why doesn't this find ads for a US/non-EU/UK brand I know is advertising?**
Meta's official API restricts non-EU/UK results to political and issue ads only — this is
documented Meta platform behavior (see Coverage above), not a bug here. The only way to see
ordinary commercial ads outside the EU/UK is Meta's own Ad Library website, which this actor
deliberately does not scrape.

**Why does a domain sometimes miss when the Page clearly exists?**
Meta's API has no domain lookup — a domain input is turned into a best-effort text search
(e.g. `nike.com` → `nike`). Try the exact Facebook Page name, or better, its numeric Page ID.

**Do I need my own Meta access token?**
To get real data, yes. This actor never ships or shares one. Apply at
[developers.facebook.com/docs/graph-api/reference/ads\_archive](https://developers.facebook.com/docs/graph-api/reference/ads_archive).
Running it without a token is allowed too: every row comes back as a `NEEDS_TOKEN` miss (never charged beyond the start fee) so you can see the shape of the output first.

**What happens if I hit Meta's rate limit?**
Affected rows come back `"status": "BLOCKED"` with Meta's own error message and guidance —
lower concurrency, submit fewer pages per run, or wait before retrying. See Features above.

### Related actors

- [Ads Presence Unified Lookup](https://apify.com/accountable_eel/ads-presence-unified-lookup) —
  Google, LinkedIn, and Meta ad presence for a company in one row (no ad-level detail).
- [Google Ads Presence Lookup](https://apify.com/accountable_eel/google-ads-presence-lookup) — the
  same enrichment-column approach for Google Ads, with domain matching.
- [LinkedIn Ads Presence Lookup](https://apify.com/accountable_eel/linkedin-ads-presence-lookup) —
  the same approach for LinkedIn Ads, by company name.

If this saved you a scrape, a rating on the Store page helps other buyers find it.

# Actor input Schema

## `pageQueries` (type: `array`):

One per line. A numeric Meta Ad Library Page ID, a Facebook Page name, or a company domain (domains are matched as best-effort text search, not a guaranteed domain-to-page mapping). Accepted formats: 156800974367001 (numeric Page ID — most reliable), Nike (Page name), nike.com (domain — best-effort). You're only charged for the ones we actually find — a miss costs nothing.

## `metaAccessToken` (type: `string`):

Apply at developers.facebook.com/docs/graph-api/reference/ads\_archive and paste your own access token here — this actor never ships or shares a token. Never logged or stored anywhere but your own run input. Without a token every row is returned as a NEEDS\_TOKEN miss so you can see the shape; nothing is fetched from Meta.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Only keep results that mention at least one of these words (e.g. a job title, a city, a product name). Leave empty to keep everything.

## `excludeKeywords` (type: `array`):

Optional. Drop any result that mentions one of these words. Leave empty to skip nothing.

## `maxResults` (type: `integer`):

Optional. Stop the run once this many results have been found — useful for a quick, cheap sample. Leave blank for no limit.

## `returnAds` (type: `boolean`):

Adds one row per active ad next to each brand's summary row: Ad Library ID and link, running-since date, platforms, body text, headline, link caption and description, languages, and EU reach where Meta shows it. Each ad row is billed as an ad (see Pricing). Every 50 ads is one more call against Meta's ~200 calls/hour token limit.

## `maxAdsPerBrand` (type: `integer`):

How many active ads to read per brand when ad rows or monitoring are on, 1 to 500. Caps the ad rows (and the ad charges) per brand. For stopped-ad alerts, set it above the brand's ad count so the whole list is read.

## `deltaMode` (type: `boolean`):

Turns the list into a competitor watchlist. Each brand row gains new ads and stopped ads (IDs active last run, not active now) since the previous run. The first run has nothing to compare against, so it just remembers each brand. Schedule the same input daily or weekly.

## `alertOnNewAds` (type: `boolean`):

Returns ad rows only for ads this watchlist has never seen, so each scheduled run delivers just the competitor's new ads. Turns on monitoring and ad rows by itself. The first run returns every ad it reads as the starting list.

## `deltaName` (type: `string`):

Leave empty to use one shared memory for this actor on your account. Name it to keep separate memories for separate schedules ("competitors-eu", "clients"). Naming a watchlist with the boxes above off still fills the since-last-run columns.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

## `maxConcurrency` (type: `integer`):

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

## Actor input object example

```json
{
  "pageQueries": [
    "nike.com",
    "156800974367001"
  ],
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "returnAds": false,
  "maxAdsPerBrand": 50,
  "deltaMode": false,
  "alertOnNewAds": false,
  "deltaName": "",
  "columns": [
    "pageId",
    "rowType",
    "pageName",
    "activeAdCount",
    "earliestActiveSince",
    "platforms",
    "sampleAdLibraryUrls",
    "euReach",
    "adListComplete",
    "newAdCount",
    "stoppedAdCount",
    "stoppedAds",
    "adArchiveId",
    "adLibraryUrl",
    "adStartedAt",
    "adStopsAt",
    "adCreatedAt",
    "adPlatforms",
    "adBodyText",
    "adTitle",
    "adLinkCaption",
    "adLinkDescription",
    "adVersionCount",
    "adLanguages",
    "adEuReach",
    "isNewAd"
  ],
  "maxConcurrency": 1
}
```

# Actor output Schema

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

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "pageQueries": [
        "nike.com",
        "156800974367001"
    ],
    "includeKeywords": [],
    "excludeKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/meta-ads-presence-lookup").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 = {
    "pageQueries": [
        "nike.com",
        "156800974367001",
    ],
    "includeKeywords": [],
    "excludeKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/meta-ads-presence-lookup").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 '{
  "pageQueries": [
    "nike.com",
    "156800974367001"
  ],
  "includeKeywords": [],
  "excludeKeywords": []
}' |
apify call accountable_eel/meta-ads-presence-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,accountable_eel/meta-ads-presence-lookup"
        }
    }
}
```

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/nrcj0nTOaBcvY5txN/builds/2pNUhRjj2AdL1d3o1/openapi.json
