# Shopify App Store Audit: Dead Privacy Policy Links (`aiqlabs/shopify-app-audit`) Actor

Shopify requires every app to publish a privacy policy and never rechecks the link. This audits the links a listing declares: policies that 404, policies that redirect to a homepage, domains nobody has registered - anyone can publish there - and dead developer and documentation links.

- **URL**: https://apify.com/aiqlabs/shopify-app-audit.md
- **Developed by:** [Ai-Q Labs](https://apify.com/aiqlabs) (community)
- **Categories:** E-commerce, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 results

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Shopify App Store Audit

Shopify requires every app in its App Store to publish a privacy policy, and shows the link on
the listing. It never revisits it.

A merchant installing an app inherits whatever that policy says, and frequently cites it in
their own store's privacy notice. So the link matters long after the developer typed it in —
and nothing in the store checks whether it still leads to a policy.

This Actor checks. It reads the links each listing declares, follows every one of them, and
asks the domain registry about the ones that failed.

### The finding this exists for

A privacy policy URL whose **domain nobody has registered**.

That is not a broken link. It is an address anyone can move into: register the domain, publish
a document, and the Shopify App Store will keep pointing every prospective merchant at your
page, under the developer's name.

Two quieter versions of the same failure sit next to it:

- The policy **404s**. The requirement is unmet and the merchant has nothing to read.
- The policy **redirects to the site's front page**. Every checker that stops at the status
  code reports a healthy `200`. A reader following the link lands on a homepage. Google treats
  this shape as a soft 404; this Actor reports it by comparing the path asked for with the path
  landed on.

### What comes out

One row per app, plus four key-value records:

| Record | What is in it |
| --- | --- |
| dataset row | the listing's facts, every declared link with its verdict, and the findings |
| `ACTION_LIST` | findings only, worst first, ranked by review count within a severity |
| `PRIVACY_REPORT` | the policies that are takeable, dead, or redirected to a homepage |
| `DEVELOPER_PROFILES` | the same findings grouped by developer, so a portfolio shows up as one |
| `SUMMARY` | counts, the query, the method, and every caveat that applies to the run |

### Input

Either name the apps or point at a category.

```json
{
  "apps": ["klaviyo-email-marketing", "https://apps.shopify.com/judgeme"],
  "categories": ["store-design-site-optimization-seo"],
  "pagesPerCategory": 2
}
```

An app is identified by its handle — the last segment of the listing URL. Full URLs are
accepted and reduced to the handle.

### What it will not do

**It does not use the store's search.** `apps.shopify.com/robots.txt` disallows any URL
carrying `q=` for every user agent. Apps are discovered through category pages, which are
permitted. A search URL passed in `apps` is refused with that reason rather than quietly
stripped of its query. robots.txt is re-read at the start of every run, and the run stops if
the listing pages stop being allowed.

**It does not claim an app is abandoned.** Shopify publishes a launch date and no update date.
There is no honest way to get staleness out of that, so no finding is built on it. (The
sibling Actors for the [App Store](https://apify.com/aiqlabs/app-store-audit) and
[Google Play](https://apify.com/aiqlabs/google-play-audit) do make that call, because those
stores publish the date.)

**It does not call a refusal a death.** A link answering 401, 403 or 429 is recorded as
`blocked` at the lowest severity: the server is up and declining to talk to a program, which
says nothing about whether a person clicking through reaches a page. Nothing here retries
behind a browser user agent — being refused is an answer, and working around it would make
this a different kind of tool.

**It does not read review text, install counts, or anything behind a login.** One request per
listing page, one per declared link, and one per failed domain.

### Severities, and why they sit where they do

Severity is chosen after measuring how common a condition is, never before. A warning that
fires on healthy input erases the rows that say "nothing to report", which is the only reason
the report is worth reading.

| Finding | Severity | Why there |
| --- | --- | --- |
| `privacy_policy_domain_unregistered` | critical | anyone can publish a policy in the developer's name at the address Shopify still shows |
| `declared_domain_unregistered` | critical | same, for the developer's own website |
| `privacy_policy_dead` | high | Shopify requires a working policy; there is nothing to read or cite |
| `developer_site_dead` | high | the listing sends readers nowhere |
| `privacy_policy_redirects_to_homepage` | medium | a `200` that is not a policy; needs a human to confirm the page moved |
| `privacy_policy_not_listed` | medium | every listing sampled had one, so this is a listing that dropped it *or* a change in the page |
| `declared_link_server_error` | medium | 5xx can be temporary — worth a second look, not a conclusion |
| `resource_link_dead` | low | FAQ, tutorial and documentation links are not required |
| `declared_link_blocked` | low | the server is alive and refused *us* |
| `declared_link_insecure` | low | only when `http://` stays `http://` — a redirect to https costs the reader nothing |
| `developer_site_offsite` | low | ordinary after a rebrand, expected after a sale; the listing does not say which |
| `app_not_found` | critical | the handle is not on the store — removed, never published, or mistyped |

Measured with this Actor's own code on 270 live listings in July 2026 — 86 from the top of six
categories and 184 from pages 14–30 of seven, where the smaller and older apps sit. Every
listing in both samples published a privacy policy. In the long tail, **1.1%** of those
policies were dead, **0.5%** sat on a domain nobody had registered, **1.6%** of developer
websites were dead — and **85.9%** of apps had nothing to report at all. The full set is in
`SUMMARY.method.baseRates`.

#### One thing it cannot tell you

The IANA RDAP bootstrap lists about 1,200 TLDs, and several common ones — `.io`, `.co`,
`.jp`, `.ro` — are not among them. There is no standard place to ask who holds
`something.io`. When a link on such a domain is dead, this Actor says it is dead and records
the registration as `unknown`, in the finding text as well as the field. It never assumes the
domain is safely held, and it never calls it free.

### Cost and courtesy

Free to run; you pay Apify only for the compute, which is small — this fetches pages and
resolves names, it does not run a browser. Requests are paced with a fixed delay and capped
concurrency, because these are ordinary page loads on someone else's server.

The User-Agent names this Actor and links to its page, so anyone reading their logs can see
who called.

### Verification

Unit tests pin the parsing and the judgement against fixed input. A separate live check runs
against the real store, real websites and real RDAP before every release, because the two
sibling Actors both shipped bugs that only real data could surface — a healthy site called
dead because it answered 403, and an entire TLD called unregistered because the wrong RDAP
server was asked.

```
npm test          # unit
npm run test:live # against the live store, sites and registries
```

# Actor input Schema

## `apps` (type: `array`):

The apps to audit. Either the handle from the listing URL (klaviyo-email-marketing) or the full URL (https://apps.shopify.com/klaviyo-email-marketing). Store search URLs are refused: apps.shopify.com/robots.txt disallows any URL carrying "q=", so this Actor never fetches one.

## `categories` (type: `array`):

Discover apps from category pages instead of listing them by hand, e.g. store-design-site-optimization-seo. Take the slug from a category URL. Category pages are what robots.txt permits; the store search is not.

## `pagesPerCategory` (type: `integer`):

How many pages of each category to walk. One page is about 25 apps. Stops early when a page adds nothing new.

## `checkDeveloperWebsite` (type: `boolean`):

Fetch the developer's own website link and report where it lands.

## `checkResourceLinks` (type: `boolean`):

Also check the supporting links a listing declares. These are reported at low severity: unlike the privacy policy, Shopify does not require them.

## `checkDomainRegistration` (type: `boolean`):

For links that do not resolve, ask RDAP whether the domain is registered at all. This is what separates "the page is gone" from "anyone can take this address". Only failed links are looked up.

## `onlyIssues` (type: `boolean`):

Leave the clean listings out of the dataset. The counts in SUMMARY still cover every app checked.

## `maxApps` (type: `integer`):

A ceiling on the run, applied after the input is resolved. Anything dropped is named in SUMMARY.notes.

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

How many requests are in flight at once. Kept low on purpose: these are ordinary page loads on someone else's server.

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

A fixed pause after each listing page, so a large run does not arrive as a burst.

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

How long any single request may take before it is recorded as unchecked.

## Actor input object example

```json
{
  "apps": [
    "klaviyo-email-marketing",
    "judgeme",
    "17track"
  ],
  "categories": [],
  "pagesPerCategory": 1,
  "checkDeveloperWebsite": true,
  "checkResourceLinks": true,
  "checkDomainRegistration": true,
  "onlyIssues": false,
  "maxApps": 200,
  "maxConcurrency": 4,
  "requestDelayMs": 600,
  "requestTimeoutSecs": 30
}
```

# Actor output Schema

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

No description

## `csv` (type: `string`):

No description

## `actionList` (type: `string`):

No description

## `privacyReport` (type: `string`):

No description

## `developerProfiles` (type: `string`):

No description

## `summary` (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 = {
    "apps": [
        "klaviyo-email-marketing",
        "judgeme",
        "17track"
    ],
    "categories": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("aiqlabs/shopify-app-audit").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 = {
    "apps": [
        "klaviyo-email-marketing",
        "judgeme",
        "17track",
    ],
    "categories": [],
}

# Run the Actor and wait for it to finish
run = client.actor("aiqlabs/shopify-app-audit").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "apps": [
    "klaviyo-email-marketing",
    "judgeme",
    "17track"
  ],
  "categories": []
}' |
apify call aiqlabs/shopify-app-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=aiqlabs/shopify-app-audit",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/JH6laeY5CNgQcadTq/builds/sIjYq5TUj7746XsnT/openapi.json
