# Apple Ad Repository Scraper (`devilscrapes/apple-ad-repository-scraper`) Actor

Query Apple's official EU DSA Ad Repository API for every App Store ad Apple served an app or developer, per country — creative assets, targeting flags, impression dates. Give an app/developer name or known App Store id. No login, no API key.

- **URL**: https://apify.com/devilscrapes/apple-ad-repository-scraper.md
- **Developed by:** [DevilScrapes](https://apify.com/devilscrapes) (community)
- **Categories:** SEO tools, Automation, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

<div align="center">
  <img src=".actor/icon.svg" width="160" alt="Devil Scrapes mark" />

## Apple Ad Repository Scraper

**💰 $6.20 / 1 000 results**  ·  pay only for results  ·  no credit card to try

*We do the dirty work so your dataset stays clean.* 😈

Query Apple's official EU DSA Ad Repository API for every App Store ad Apple served an app or developer, per country — creative assets, targeting flags, impression dates. Give an app/developer name or known App Store id. No login, no API key.

</div>

***

### 🎯 What this scrapes

Under the EU Digital Services Act (Article 39), Apple is legally required to
publish every App Store ad it delivered to EU users in a public **Ad
Repository**. This Actor turns that repository into a queryable dataset:
give it an app or developer name (or a known App Store id if you already
have one) and it resolves the entity, then pulls every ad Apple served for
it — per EU country, with the creative assets, targeting flags, and
impression dates attached — instead of you hand-paging Apple's own web UI
one app at a time.

Batch multiple apps or developers in a single run. Each target is
fault-isolated — one app with zero ads or a temporary hiccup never sinks
the others.

### 🔥 What we handle for you

- 🛡️ **Browser fingerprint rotation** — `curl-cffi` impersonates real Chrome / Firefox TLS handshakes on every request.
- 🔁 **Retries with exponential backoff** on `408 / 429 / 503` — up to 5 attempts per page, `Retry-After` honoured.
- 🔎 **Two-stage resolution** — hand us a name and we resolve it to the right App Store entity ourselves, or skip straight to an ad-list query if you already know the id.
- 📄 **Real pagination** past Apple's 50-item page size, up to the cap you set.
- 🧊 **Clean, typed dataset rows** — Pydantic-validated, ISO-8601 timestamps, stable ids, JSON / CSV / Excel export straight from the Apify Console.
- 💰 **Pay-Per-Event pricing** — you only pay for ad rows that hit your dataset. No data, no charge.

### 💡 Use cases

- ASO agencies tracking which App Store ad creatives a client or rival is currently running in the EU.
- Competitive intel — see exactly what formats, copy, and targeting a rival app is using, per country.
- Ad-tech and policy research using Apple's own DSA transparency disclosures as a structured dataset.
- Periodic monitoring — schedule a run and diff successive pulls to catch new creative launches.

### ⚙️ How to use it

1. Click **Try for free** at the top of the page.
2. Enter one or more app/developer names in `entityNames`, or known App Store ids in `entities` — at least one of the two is required.
3. Optionally narrow `countryOrRegions` and `datePreset`, or leave the defaults.
4. Click **Start**. Output streams into the run's dataset as each target resolves.
5. Export from **Storage → Dataset** as JSON, CSV, or Excel — or fetch via the API.

### 📥 Input

| Field | Type | Required | Default | Notes |
|---|---|:--:|---|---|
| `entityNames` | `array` | no\* | `[]` | App/developer names, resolved via Apple's own entity search. |
| `entities` | `array` | no\* | `[]` | Known `{id, type}` pairs — skips name resolution. |
| `countryOrRegions` | `array` | no | `[]` | ISO alpha-2 EU codes. Empty = every EU country Apple's own country endpoint returns, fetched live. |
| `datePreset` | `string` | no | `"LAST_90_DAYS"` | `LAST_90_DAYS` / `LAST_180_DAYS` / `LAST_YEAR`. |
| `maxItemsPerQuery` | `integer` | no | `500` | Cap on ad rows collected per resolved target. |
| `proxyConfiguration` | `object` | no | `{"useApifyProxy": false}` | Apify Proxy routing — off by default, see Limitations. |

\* At least one of `entityNames` / `entities` must be populated — validated before any charge fires.

#### Example input

```json
{
  "entityNames": ["Duolingo"],
  "entities": [{"id": 570060128, "type": "APP"}],
  "countryOrRegions": ["DE", "FR", "IT", "ES", "NL"],
  "datePreset": "LAST_YEAR",
  "maxItemsPerQuery": 120
}
```

### 📤 Output

Every row is one ad, in one country — the API itself scopes each record to
a single `countryOrRegion`, so no extra fan-out is needed on our end.

| Field | Type | Notes |
|---|---|---|
| `adId` | `string` | Apple's stable ad identifier. |
| `appId` / `appName` | `integer` / `string` | App Store id and name shown in the ad. |
| `developerId` / `developerName` / `legalName` | `integer` / `string` / `string` | Advertiser identity, including the DSA-disclosed legal entity name. |
| `placement` / `format` / `subFormat` | `string` | Where and how the ad ran, e.g. `APPSTORE_SEARCH_RESULTS` / `"Icon + Asset Ad"`. |
| `countryOrRegion` | `string` | EU country this row's impressions were served in. |
| `firstImpressionDate` / `lastImpressionDate` | `string` | Date range Apple recorded impressions for this ad, in this country. |
| `adBannerSubtitle` / `adBannerPrimaryCategory` / `adBannerShortDescription` / `adBannerPromotionalText` | `string` | Ad banner creative text fields. |
| `adBannerInAppPurchases` / `adBannerEditorialBadge` | `boolean` | Ad banner disclosure flags. |
| `adAssetPictureUrls` / `adAssetVideoUrls` / `appIconVariations` | `array` | Creative asset URLs — empty list when none, never null. |
| `audienceAgeTarget` / `audienceGenderTarget` / `audienceLocationTarget` / `audienceCustomerTypeTarget` | `boolean` | Which audience-targeting dimensions this ad used. |
| `dataEndDate` | `string` | Apple's own data-freshness cutoff for this query — see Limitations. |
| `searchedEntityId` / `searchedEntityType` / `searchedEntityName` | `integer` / `string` / `string` | The resolved target this row came from. |
| `scrapedAt` | `string` | ISO-8601 UTC timestamp when the row was scraped. |

#### Example output

```json
{
  "adId": "a1b2c3",
  "appId": 570060128,
  "appName": "Duolingo: Language Lessons",
  "developerName": "Duolingo",
  "placement": "APPSTORE_SEARCH_RESULTS",
  "format": "Icon + Asset Ad",
  "countryOrRegion": "DE",
  "firstImpressionDate": "2026-06-01",
  "lastImpressionDate": "2026-08-30",
  "adBannerSubtitle": "Lerne kostenlos",
  "adBannerPrimaryCategory": "Education",
  "adBannerInAppPurchases": true,
  "adAssetPictureUrls": ["https://is5-ssl.mzstatic.com/.../asset1.png"],
  "audienceLocationTarget": true,
  "dataEndDate": "2026-09-14",
  "searchedEntityId": 570060128,
  "searchedEntityType": "APP",
  "searchedEntityName": "Duolingo",
  "scrapedAt": "2026-09-21T00:00:00Z"
}
```

### 💰 Pricing

Pay-Per-Event — you pay only when these events fire:

| Event | USD | What it is |
|---|---:|---|
| `actor-start` | $0.20 | One-off warm-up charge per run |
| `ad-result` | $0.006 | Charged once per ad row (one ad in one country) written to the dataset |

Example: 1 000 ad rows costs $0.20 start + 1 000 × $0.006 = **$6.20** total.
No subscription, no minimum, no card to start — Apify gives every new
account $5 of free credit.

### 🚧 Limitations

- **EU-only by design, not by weakness.** The Ad Repository exists because
  the EU's Digital Services Act mandates it — it covers App Store ads shown
  to EU users only. That's precision, not a gap: it's the exact scope
  Apple's legal disclosure obligation covers.
- **Apple's own data lags ~7 days.** Every row carries `dataEndDate`, the
  cutoff Apple itself publishes for that query — so you always know exactly
  how fresh the data is instead of guessing. This is Apple's documented
  freshness window, not something we can shrink.
- **Two endpoints are deliberately out of scope for v1**: per-ad
  locale/icon variations and Apple's advertising-restrictions endpoint.
  Both are real, sellable datasets in their own right — documented as
  follow-ups, not silently dropped.
- No proxy is used by default (`proxyConfiguration.useApifyProxy: false`)
  — live wire probes found zero anti-bot surface on this API. You can opt
  into a named Apify Proxy group yourself if that ever changes.

### ❓ FAQ

**Is this legal?**

Yes — the Ad Repository is Apple's own public disclosure, published
because EU law requires it. We only fetch what Apple already makes
publicly queryable.

**Why do I need both `entityNames` and `entities`?**

You don't — either alone works. `entityNames` is the convenient path (we
resolve the name to Apple's id for you); `entities` skips that lookup if
you already know the numeric App Store id and entity type.

**Why is `dataEndDate` a few days behind today?**

That's Apple's own freshness cutoff for the Ad Repository, not a delay we
introduce — we surface it on every row so you always know exactly how
current your pull is.

**How do I export to Sheets?**

After the run, click *Storage → Dataset → Export* and pick CSV. Google
Sheets imports it directly.

### 💬 Your feedback

Spotted a bug, hit a weird edge case, or need a new field? Open an
issue on the Actor's **Issues** tab on Apify Console — we ship
fixes weekly and we read every report.

***

<div align="center">

Built by **[Devil Scrapes](https://apify.com/DevilScrapes)** 😈 — a small fleet of
opinionated public-data Actors. Honest pricing, real engineering, zero fine print.

</div>

# Changelog

This Actor's version history is a separate document: https://apify.com/devilscrapes/apple-ad-repository-scraper/changelog.md

# Actor input Schema

## `entityNames` (type: `array`):

App or developer names to resolve via Apple's entity search, one search per name — the best match's id/type is then used to query the Ad Repository. Each name must be at least 2 characters. Leave empty if you already know the numeric id(s) and use <code>entities</code> instead.

## `entities` (type: `array`):

Known App Store entity ids, skipping the name-resolution step. Each item is <code>{"id": \<numeric App Store id>, "type": "APP" or "DEVELOPER"}</code>. At least one of <code>entityNames</code> / <code>entities</code> must be populated.

## `countryOrRegions` (type: `array`):

ISO alpha-2 EU country codes to scope the search to, e.g. <code>DE</code>, <code>FR</code>. Leave empty to use every EU country/region Apple's own <code>/countries-or-regions</code> endpoint returns at run time (not a hardcoded list).

## `datePreset` (type: `string`):

Apple's supported ad-activity lookback window.

## `maxItemsPerQuery` (type: `integer`):

Cap on ad rows collected per resolved app/developer target before its pagination walk stops early (cost/safety cap).

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

Apify Proxy routing. Defaults to direct (no proxy) — this API satisfies a binding EU legal disclosure obligation and live wire probes (chrome131 + firefox133 UA) showed zero anti-bot surface, so a proxy buys nothing here by default. Opt into a named group yourself if Apple's posture ever changes.

## Actor input object example

```json
{
  "entityNames": [
    "Duolingo"
  ],
  "entities": [
    {
      "id": 1641486558,
      "type": "APP"
    }
  ],
  "countryOrRegions": [
    "DE",
    "FR",
    "IT",
    "ES",
    "NL"
  ],
  "datePreset": "LAST_YEAR",
  "maxItemsPerQuery": 120,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `datasetItems` (type: `string`):

All dataset items as JSON.

## `datasetItemsCsv` (type: `string`):

Same data exported to CSV.

## `datasetView` (type: `string`):

Open the run dataset in the Console.

# 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 = {
    "entityNames": [
        "Duolingo"
    ],
    "entities": [
        {
            "id": 1641486558,
            "type": "APP"
        }
    ],
    "countryOrRegions": [
        "DE",
        "FR",
        "IT",
        "ES",
        "NL"
    ],
    "datePreset": "LAST_YEAR",
    "maxItemsPerQuery": 120,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("devilscrapes/apple-ad-repository-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 = {
    "entityNames": ["Duolingo"],
    "entities": [{
            "id": 1641486558,
            "type": "APP",
        }],
    "countryOrRegions": [
        "DE",
        "FR",
        "IT",
        "ES",
        "NL",
    ],
    "datePreset": "LAST_YEAR",
    "maxItemsPerQuery": 120,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("devilscrapes/apple-ad-repository-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 '{
  "entityNames": [
    "Duolingo"
  ],
  "entities": [
    {
      "id": 1641486558,
      "type": "APP"
    }
  ],
  "countryOrRegions": [
    "DE",
    "FR",
    "IT",
    "ES",
    "NL"
  ],
  "datePreset": "LAST_YEAR",
  "maxItemsPerQuery": 120,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call devilscrapes/apple-ad-repository-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,devilscrapes/apple-ad-repository-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/npz9wo0UPWfQXln9i/builds/kXOgNTy54oj6IdltG/openapi.json
