Skip to main content

AASA File Format for iOS Universal Links: The Complete Guide

Universal Links — iOS's mechanism for opening your app from ordinary HTTPS URLs — rest entirely on one JSON file. The Apple App Site Association file (universally called AASA) sits on your web server and declares which apps may open which paths on your domain. Get it right and your links glide from Safari, Messages, and Mail straight into the app. Get it wrong and iOS silently ignores you — no error, no log, just links that keep opening in the browser.

This is a working reference for the file: its format, its matching rules, its hosting requirements, and the failure modes that bite people.

What the AASA file is for

iOS will only hand an HTTPS URL to an app when it can prove the app and the website belong to the same party. The proof is the AASA file: served from your domain, it names your app's signing identity, and iOS verifies it when your app is installed or updated. If iOS later sees a link to a path the file covers, it opens your app instead of Safari — and if the app isn't installed, the URL simply loads your website. Universal Links never dead-end.

The JSON format

The current format (iOS 13 and later) looks like this:

{
  "applinks": {
    "details": [
      {
        "appIDs": ["ABCDE12345.com.example.myapp"],
        "components": [
          { "/": "/product/*", "comment": "Product pages open in app" },
          { "/": "/order/*", "comment": "Order tracking" },
          { "/": "/admin/*", "exclude": true, "comment": "Never open in app" },
          { "/": "/blog/*", "exclude": true }
        ]
      }
    ]
  },
  "webcredentials": {
    "apps": ["ABCDE12345.com.example.myapp"]
  }
}

Under applinks.details, each entry pairs a list of appIDs with a list of components — the path rules. The order matters: iOS evaluates components top to bottom, so place your exclude rules before any broad wildcard that would otherwise match.

The appIDs field

Each app ID is your Apple Team ID concatenated with your bundle identifier: ABCDE12345.com.example.myapp. You can list several — a production app and a beta build, for instance — under one appIDs array, and multiple details entries can cover different apps with different path rules.

Path patterns and matching rules

The "/" component value matches the URL's path, with two wildcards:

  • * matches zero or more characters within a path component — /product/* matches /product/12345.
  • ? matches exactly one character.

Further rules worth knowing:

  • A bare { "/": "*" } matches every path on the domain.
  • exclude: true blocks matching for that pattern — the modern replacement for the old NOT prefix.
  • Matching is case-sensitive by default; set caseSensitive: false per component if you need lenient matching.
  • Query strings and fragments are ignored — /product/12345?utm_source=email matches the /product/* rule on path alone.

The legacy format, briefly

Older AASA files (and older guides) use appID (singular, a string) and a paths array with NOT prefixes:

{ "applinks": { "apps": [], "details": [
  { "appID": "ABCDE12345.com.example.myapp", "paths": ["*", "NOT /admin/*"] }
] } }

iOS still understands it, but prefer the components format for new files — it's more expressive and it's where Apple's tooling is focused.

applinks vs. webcredentials

They live in the same file but do different jobs. applinks powers Universal Links — deep linking. webcredentials lets Safari and apps offer your website's passwords and passkeys in AutoFill. Including webcredentials doesn't affect deep linking either way; it's just convenient to co-locate.

Hosting requirements

This is where most AASA setups fail, and iOS fails silently:

  • Exact path: https://yourdomain.com/.well-known/apple-app-site-association — no .json extension.
  • HTTPS only. iOS will not fetch the file over plain HTTP.
  • No redirects. The file must be served directly at that URL; a 301/302 elsewhere breaks verification.
  • Content-Type: application/json — some servers default to application/octet-stream, which iOS rejects.
  • Valid JSON, under 128 KB. Invalid JSON fails quietly; iOS caches the file, so a bad version can linger.

A complete example: two apps on one domain

{
  "applinks": {
    "details": [
      {
        "appIDs": ["ABCDE12345.com.example.myapp"],
        "components": [
          { "/": "/product/*" },
          { "/": "/order/*" },
          { "/": "/admin/*", "exclude": true },
          { "/": "/api/*", "exclude": true }
        ]
      },
      {
        "appIDs": ["ABCDE12345.com.example.myapp.beta"],
        "components": [
          { "/": "/product/*" }
        ]
      }
    ]
  }
}

Validating your file

  1. Check the raw response: curl -I https://yourdomain.com/.well-known/apple-app-site-association — confirm 200, HTTPS, application/json, no redirect.
  2. Validate the JSON — any linter; a trailing comma is all it takes.
  3. Check the entitlement: the app must carry associated-domains with applinks:yourdomain.com, and the app ID in the file must match Team ID + bundle ID exactly.
  4. Test on device: paste a link into Notes or Messages and long-press it — "Open in YourApp" should appear, and a plain tap should open the app, not Safari.

Troubleshooting common issues

  • Links always open in Safari: usually a mismatch between the entitlement's domain and the file's location, or the file failing validation. Re-check with curl before blaming the app.
  • Worked yesterday, broken today: iOS caches the AASA aggressively and re-fetches on app install/update or occasionally in the background. After changing the file, reinstall the app (or bump the build) to force a re-fetch.
  • Wrong Content-Type: if your host serves the file as octet-stream, fix the server config — the JSON itself is irrelevant until the header is right.
  • Some paths open the app, others don't: re-read your component order; an early broad wildcard can shadow a later rule, and a forgotten exclude on an admin path opens it in the app by mistake.

Letting a platform host it for you

When you run your links through DecTog, the AASA file for your link domain is generated from your app's configuration in the dashboard — bundle ID and paths — and served at the correct path with the correct headers, so there's no JSON to hand-maintain and no silent-failure debugging. You keep ownership of the domain; DecTog just serves the file.

Related reading: assetlinks.json — the Android counterpart, and What is Deep Linking? for the wider context.