# App Links & Universal Links Auditor (`phoenix2810/app-links-auditor`) Actor

Audit a public domain's Apple App Site Association and Android assetlinks.json files in one API call. Validates Universal Links and App Links configuration, returns per-platform breakdown, readiness score, grade, and recommendations.

- **URL**: https://apify.com/phoenix2810/app-links-auditor.md
- **Developed by:** [Sanskar Jaiswal](https://apify.com/phoenix2810) (community)
- **Categories:** Developer tools, SEO tools, Open source
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## 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

## App Links & Universal Links Auditor

Audits a public domain's Apple App Site Association file (Universal Links) and Android assetlinks.json file (App Links) in one API call. Fetches both well-known files, validates them against the Apple and Google requirements, and returns a per-platform breakdown, a readiness score from 0 to 100, a letter grade, issues, and recommendations as structured JSON.

### Use cases

- Mobile app teams verifying domain association files after releases, CMS migrations, and CDN cutovers
- iOS developers validating AASA structure before submitting an app that uses Universal Links
- Android developers validating assetlinks.json statements before enabling App Links verification
- Agency consultants running recurring deep-link posture checks across client domains
- QA pipelines gating deploys on broken or malformed association files
- Web engineering teams confirming association files are served over HTTPS, without redirects, and without signing (no file extension on AASA)

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| startUrl | string | (required) | Public domain or base URL to audit. A bare hostname gets the https scheme prepended. Private IP ranges, URL credentials, and non-HTTP schemes are rejected. |
| timeoutSeconds | integer | 10 | Request timeout per file, from 3 to 30 seconds. |
| maxBytes | integer | 131072 | Maximum response body size to download and parse per file, from 1 KB to 512 KB. |

### Output

One dataset item per run:

| Field | Type | Description |
| --- | --- | --- |
| inputUrl | string | URL as provided in the input. |
| finalUrl | string | Normalized base URL after validation. |
| https | boolean | Whether the base URL uses HTTPS. |
| ok | boolean | Whether all HTTP requests completed without network errors. |
| checkedAt | string | ISO 8601 timestamp of the check. |
| aasaChecked | boolean | Whether the Apple check ran (always true). |
| aasaFound | boolean | Whether the AASA file was found (200 on the well-known path or the legacy root path). |
| aasaUrl | string | The AASA URL that produced the result. |
| aasaStatus | integer or null | HTTP status of the AASA response. |
| aasaHttps | boolean or null | Whether the AASA file was served over HTTPS. |
| aasaContentType | string or null | Content-Type of the AASA response. |
| aasaJsonValid | boolean or null | Whether the AASA body parsed as JSON. |
| aasaParseError | string or null | JSON parse error for the AASA body, if any. |
| aasaAppCount | integer | Number of entries in the legacy top-level apps array (modern format should use none). |
| aasaDetailCount | integer | Number of entries in applinks.details. |
| aasaApplinksPresent | boolean | Whether the applinks block is present. |
| aasaValidAppIds | integer | Number of validly formatted app IDs in the legacy top-level apps array. |
| aasaInvalidAppIds | array | Invalidly formatted app IDs in the legacy top-level apps array. |
| aasaWebcredentialsCount | integer | Number of apps in the webcredentials block. |
| aasaAppclipsPresent | boolean | Whether a valid appclips block is present. |
| aasaDetails | array | Per-detail analysis: appIDs, invalid appIDs, components, component issues, legacy paths, and per-entry issues. |
| aasaRedirected | boolean | Whether the AASA file was served through a redirect. |
| aasaRedirectIssue | boolean | Whether the redirect is flagged as an issue (iOS may fail association behind redirects). |
| aasaIssues | array | All AASA issues found. |
| assetlinksChecked | boolean | Whether the Android check ran (always true). |
| assetlinksFound | boolean | Whether assetlinks.json was found (200). |
| assetlinksUrl | string | The assetlinks.json URL. |
| assetlinksStatus | integer or null | HTTP status of the assetlinks.json response. |
| assetlinksHttps | boolean or null | Whether assetlinks.json was served over HTTPS. |
| assetlinksContentType | string or null | Content-Type of the assetlinks.json response. |
| assetlinksJsonValid | boolean or null | Whether the assetlinks.json body parsed as JSON. |
| assetlinksParseError | string or null | JSON parse error for the assetlinks.json body, if any. |
| assetlinksTargetCount | integer | Number of statements in assetlinks.json. |
| assetlinksPackageCount | integer | Number of unique valid Android package names across statements. |
| assetlinksPackages | array | The unique valid Android package names. |
| assetlinksValidFingerprints | integer | Number of validly formatted SHA-256 certificate fingerprints. |
| assetlinksInvalidFingerprints | array | Invalidly formatted fingerprints. |
| assetlinksValidRelations | integer | Number of valid delegate\_permission relations. |
| assetlinksInvalidRelations | array | Invalidly formatted relations. |
| assetlinksStatements | array | Per-statement analysis: namespace, package, fingerprints, relations, and per-statement issues. |
| assetlinksIssues | array | All assetlinks.json issues found. |
| score | integer | Readiness score from 0 to 100. |
| grade | string | Letter grade from A+ to F. |
| issues | array | Combined issues from both platforms. |
| recommendations | array | Actionable fixes for each platform. |
| error | string or null | Error message when the audit could not complete. |

### Example input

```json
{
    "startUrl": "https://www.wikipedia.org",
    "timeoutSeconds": 10,
    "maxBytes": 131072
}
```

### Example output

```json
{
    "inputUrl": "https://www.wikipedia.org",
    "finalUrl": "https://www.wikipedia.org/",
    "https": true,
    "ok": true,
    "checkedAt": "2026-09-28T09:15:00.000Z",
    "aasaChecked": true,
    "aasaFound": true,
    "aasaUrl": "https://www.wikipedia.org/.well-known/apple-app-site-association",
    "aasaStatus": 200,
    "aasaHttps": true,
    "aasaContentType": "application/json",
    "aasaJsonValid": true,
    "aasaParseError": null,
    "aasaAppCount": 0,
    "aasaDetailCount": 1,
    "aasaApplinksPresent": true,
    "aasaValidAppIds": 0,
    "aasaInvalidAppIds": [],
    "aasaWebcredentialsCount": 0,
    "aasaAppclipsPresent": false,
    "aasaDetails": [
        {
            "index": 0,
            "appIDs": ["TEAMID123.org.wikipedia.wiki"],
            "appIDCount": 1,
            "invalidAppIDs": [],
            "components": [{ "index": 0, "path": "/*", "issues": [] }],
            "componentCount": 1,
            "componentIssues": 0,
            "paths": [],
            "issues": []
        }
    ],
    "aasaRedirected": false,
    "aasaRedirectIssue": false,
    "aasaIssues": [],
    "assetlinksChecked": true,
    "assetlinksFound": true,
    "assetlinksUrl": "https://www.wikipedia.org/.well-known/assetlinks.json",
    "assetlinksStatus": 200,
    "assetlinksHttps": true,
    "assetlinksContentType": "application/json",
    "assetlinksJsonValid": true,
    "assetlinksParseError": null,
    "assetlinksTargetCount": 1,
    "assetlinksPackageCount": 1,
    "assetlinksPackages": ["org.wikipedia.wiki"],
    "assetlinksValidFingerprints": 1,
    "assetlinksInvalidFingerprints": [],
    "assetlinksValidRelations": 1,
    "assetlinksInvalidRelations": [],
    "assetlinksStatements": [
        {
            "index": 0,
            "namespace": "android_app",
            "package_name": "org.wikipedia.wiki",
            "sha256Fingerprints": ["AA:BB:...:99"],
            "fingerprintCount": 1,
            "relations": ["delegate_permission/common.handle_all_urls"],
            "issues": []
        }
    ],
    "assetlinksIssues": [],
    "score": 97,
    "grade": "A+",
    "issues": [],
    "recommendations": [
        "Both association files are well-formed. Schedule this audit periodically to catch deploy and CDN regressions."
    ],
    "error": null
}
```

### Security

- Fetches only the three association paths on the audited domain: /.well-known/apple-app-site-association, /apple-app-site-association (legacy fallback), and /.well-known/assetlinks.json
- HTTP and HTTPS only; rejects URL credentials, non-HTTP schemes, private IPv4 and IPv6 literals, and hostnames whose DNS resolves to private IP ranges
- Redirects are revalidated against the SSRF rules before following; at most 3 redirects per file
- Response bodies are capped by the maxBytes input (default 128 KB, hard max 512 KB)
- Does not fetch appIDs, package names, or any URLs referenced inside the association files
- No login, no JavaScript execution, no cookies, no stored page content

### Pricing

| Event | Price | Description |
| --- | --- | --- |
| Actor start | $0.005 | Charged once per run. |
| Domain audited | $0.01 | Charged per domain audited (both platforms in one run). |

A typical single-domain audit costs $0.015 per run.

### FAQ

**Does the actor check both Apple and Android files in one run?**
Yes. Every run fetches the AASA file (well-known path with legacy root fallback) and assetlinks.json, and returns per-platform results plus a combined score.

**Does the actor validate the AASA JSON structure?**
Yes. It checks the applinks block, apps and details arrays, appID format (TEAMID.bundle.identifier), component shape (the "/" key, exclude, and allowed keys), legacy paths arrays, and the optional webcredentials and appclips blocks.

**Does the actor validate assetlinks.json statements?**
Yes. It checks that the file is a JSON array of statements, each with a relation list, a target with namespace android\_app, a valid Android package name, and SHA-256 certificate fingerprints in hex or base64 format.

**What about redirects?**
iOS does not reliably follow redirects for the AASA file. The actor follows up to 3 redirects to complete the audit but flags the redirect as an issue when the AASA file is served behind one.

**Can I audit multiple domains?**
Run the actor once per domain, or schedule it across your domain list for recurring monitoring.

**Does the actor open the app or test deep links end to end?**
No. It audits the server-side association files, which are the most common point of failure. End-to-end deep-link testing requires a device.

# Actor input Schema

## `startUrl` (type: `string`):

Public domain or base URL to audit. The actor fetches /.well-known/apple-app-site-association, the legacy /apple-app-site-association path, and /.well-known/assetlinks.json. HTTP and HTTPS only. Private IP ranges are blocked.

## `timeoutSeconds` (type: `integer`):

Timeout for each HTTP request to the well-known endpoints.

## `maxBytes` (type: `integer`):

Maximum response body size to download and parse per file. Association files are JSON and should be small.

## Actor input object example

```json
{
  "startUrl": "https://example.com",
  "timeoutSeconds": 10,
  "maxBytes": 131072
}
```

# 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 = {
    "startUrl": "https://example.com"
};

// Run the Actor and wait for it to finish
const run = await client.actor("phoenix2810/app-links-auditor").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 = { "startUrl": "https://example.com" }

# Run the Actor and wait for it to finish
run = client.actor("phoenix2810/app-links-auditor").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 '{
  "startUrl": "https://example.com"
}' |
apify call phoenix2810/app-links-auditor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,phoenix2810/app-links-auditor"
        }
    }
}
```

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/hYIyNCjKDfsegx6IG/builds/PwERuWQa69rVhWvyR/openapi.json
