On this page · 10 sections
Summary. On 14 August 2026 Amazon SES added the ses:custom-path HTML attribute, which inserts a fixed path segment into click-tracking URLs so iOS Universal Links and Android App Links can match them. The tracking URL changes from /CL0/{encodedUrl}/{index}/{messageId}/{hmac} to /CL1/{yourpath}/{encodedUrl}/{index}/{messageId}/{hmac}. The value must be 1-32 characters, case-sensitive, drawn from letters, digits, hyphens, periods and the _ character. Three things the announcement does not tell you: an invalid value falls back to the default /CL0/ format with no error, the AWS example apple-app-site-association file uses Apple's legacy paths array rather than the current components array, and the path never appears in click event data. SES is available in the 3 pricing tiers documented in the SES Developer Guide, and open and click metrics are retained for 60 days.
For 5 years the practical answer to "can we track email clicks and still open the app?" has been no. Click tracking rewrites every <a href> to a redirect domain, and both mobile platforms decide which app opens a URL by matching the host and the path prefix against a verification file. A rewritten URL with an opaque path matched nothing, so the link opened Safari or Chrome instead of the app. Teams disabled click tracking, or accepted the browser bounce, or ran a second redirect service alongside SES.
ses:custom-path closes that gap. It is a small attribute with a large blast radius, because getting it wrong produces a link that works perfectly in a browser and silently never opens the app.
What actually changed
The attribute goes on individual anchor tags in the email HTML:
SES strips the attribute before delivery, so recipients never see it. What they receive is a link to your custom redirect domain carrying the segment you specified. The AWS SES Developer Guide states the two URL shapes plainly:
| Configuration | Tracking URL shape | App can match it |
|---|---|---|
| Default, no attribute | https://{domain}/CL0/{encodedUrl}/{index}/{messageId}/{hmac} |
No - path prefix is /CL0/ for every link in every campaign |
ses:custom-path="myapp" |
https://{domain}/CL1/myapp/{encodedUrl}/{index}/{messageId}/{hmac} |
Yes - path prefix /CL1/myapp/ is stable and yours |
| Invalid value | https://{domain}/CL0/{encodedUrl}/{index}/{messageId}/{hmac} |
No - and nothing in the API response says so |
That third row is the one to plan around. The Developer Guide says the attribute "falls back to the default format if the value is invalid (same as omitting ses:custom-path)". A value of 33 characters, or one containing a slash or a plus sign, produces a perfectly valid tracked link that your verification file will never match. The send succeeds. CloudWatch records a click. The recipient lands in a browser.
The prerequisites are not trivial either. Per the Developer Guide, you need a custom redirect domain already configured (Part 1), a configuration set pointing at it through the TrackingOptions property (Part 2), and open or click event types selected in an event destination (Part 3). Only then does Part 4, the new deep linking section, apply. If you are still using the SES-operated awstrack.me domains, ses:custom-path does nothing for you, because you cannot host a verification file on a domain AWS owns.
The AASA example is the legacy format
Here is where the AWS documentation and Apple's documentation disagree.
The SES Developer Guide tells you to host this at https://{your-custom-redirect-domain}/.well-known/apple-app-site-association:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "{TEAMID}.{com.example.myapp}",
"paths": ["/CL1/{myapp}/*"]
}
]
}
}
Apple's current "Supporting associated domains" documentation does not use appID and paths. It uses an appIDs array and a components array, where each component is a dictionary with / for path matching, # for fragment matching and ? for query parameter matching, plus an optional exclude flag. Apple's own example matches paths with entries like {"/": "/buy/*"}.
Both forms still work on current iOS. The paths array is the older syntax and Apple's documentation has moved on from it. The practical consequence is that a team following the AWS page lands on a format that does not match anything in Apple's guide, cannot express an exclusion, and cannot filter on a query parameter. If your redirect domain also serves web content you want to stay in the browser, the AWS example gives you no way to say so. Written against components, the same rule becomes {"/": "/CL1/myapp/*"} and sits beside whatever other rules that domain already needs.
The second thing the AWS page omits is timing. Apple states that its content delivery network requests the apple-app-site-association file for your domain within 24 hours, and that devices check for updates approximately once per week after app installation. Since macOS 11 and iOS 14, apps do not fetch the file from your web server at all; they fetch it from an Apple-managed CDN. So a ses:custom-path value you change on Monday is not a Monday change for installed users. Plan the path segment once and leave it alone.
Android does not read the path from the JSON file at all
The asymmetry between the two platforms is the part most likely to cost a sprint.
On iOS the path pattern lives in the server-hosted AASA file. On Android it does not. The SES Developer Guide is correct on this point and says so in a note: Android path-prefix matching is configured with an intent filter in AndroidManifest.xml using android:pathPrefix="/CL1/{myapp}/", not in assetlinks.json. The assetlinks.json file carries only the association - package_name, sha256_cert_fingerprints, and the delegate_permission/common.handle_all_urls relation.
| Platform | Where the path prefix lives | Where the association lives | Change requires |
|---|---|---|---|
| iOS Universal Links | apple-app-site-association on the redirect domain |
Same file, appIDs |
Server edit, then up to 24h CDN fetch and roughly weekly device recheck |
| Android App Links (all versions) | AndroidManifest.xml intent filter, android:pathPrefix |
assetlinks.json on the redirect domain |
App release and store rollout |
| Android 15+ dynamic rules | assetlinks.json, dynamic_app_link_components |
assetlinks.json |
Server edit only, within manifest-declared scope |
That last row is the escape hatch AWS does not mention. Android 15 (API level 35) and later, on devices with Google services installed, periodically retrieve assetlinks.json and merge a dynamic_app_link_components relation extension with the static manifest rules. It supports path, fragment and query matchers and an exclude flag, which is the closest Android equivalent to Apple's components. Two constraints matter: dynamic rules can only narrow the scope declared in the manifest, never widen it, and if any field in the file is malformed or empty, Android discards the dynamic rules entirely and falls back to the manifest. Devices on Android 14 (API level 34) and earlier ignore the relation extension fields completely.
There is also a hosting requirement that collides directly with how SES custom redirect domains are built. Android's documentation states that assetlinks.json must be served with content-type application/json, must be reachable over HTTPS, and "must be accessible without any redirects (no 301 or 302 redirects)". An SES custom redirect domain on HTTPS is, per the Developer Guide, a CDN such as Amazon CloudFront pointing at a regional SES tracking origin like r.us-east-1.awstrack.me, with the Host header forwarded. Your /.well-known/assetlinks.json and /.well-known/apple-app-site-association have to be served from that same hostname without a redirect, which means a CloudFront behaviour that serves those two paths from your own origin rather than forwarding them to SES. Apple applies the same rule: the file must be hosted "using https:// with a valid certificate and with no redirects".
Your analytics will not tell you which links were deep-linkable
The Developer Guide is explicit: "The ses:custom-path value does not appear in click event data. To identify which link was clicked in events, use ses:tags."
So the attribute that determines whether a click opens your app is invisible in the event stream that tells you the click happened. If you want to measure the rollout, you have to tag the same links twice - once with ses:custom-path for the operating system and once with ses:tags for yourself - and keep the two in sync by convention. Nothing enforces it.
The measurement window is fixed too. SES collects open and click metrics for 60 days after each email is sent, and the SES API has no method for retrieving them; you read them from CloudWatch, for example with aws cloudwatch get-metric-statistics --namespace AWS/SES --metric-name Click. Every open and click is counted at recipient level, and repeat clicks on the same link each count as a separate event.
One more caveat that predates this launch and still applies: SES states that event counts can be skewed by the recipient's email provider, including providers that cache images or links, in which case SES counts only the initial open. Deep link attribution built on click counts inherits that skew.
How to tell if this affects you
You are in scope if all of the following are true: you send transactional or marketing email through Amazon SES, you have click tracking enabled through a configuration set, you have a published iOS or Android app, and you expect links in those emails to open the app. If you disabled click tracking specifically to keep deep links working, this is the change that lets you turn it back on.
You are not in scope if you use the default SES-operated tracking domains, because the verification files have to live on a domain you control.
The work, in order:
- Confirm the custom redirect domain is on HTTPS and returns the expected
x-amz-ses-regionandx-amz-ses-request-protocolheaders. The Developer Guide gives the check:curl --head https://{custom.domain.com}/favicon.ico.
- Pick one path segment and freeze it. 1-32 characters, case-sensitive, letters, digits, hyphens, periods and the
_character only.
- Serve both
.well-knownfiles from that hostname with no redirect and, forassetlinks.json, content-typeapplication/json.
- Write the AASA against Apple's
componentsformat rather than the AWS example'spathsarray, unless you have a specific reason to stay on the legacy syntax.
- Ship the Android intent filter with
android:pathPrefix="/CL1/{yourpath}/"in a release build. This is an app release, not a server change.
- Verify with
adb shell pm verify-app-links --re-verify PACKAGE_NAMEand thenadb shell pm get-app-links PACKAGE_NAME, looking for a state ofverifiedrather thannoneorlegacy_failure.
- Add a matching
ses:tagsvalue so the click event stream can be reconciled against the deep link rollout.
Step 5 is the one that sets the schedule. On iOS the path pattern is a file edit. On Android it is a store release, and if you get the prefix wrong you ship another one. Decide the segment before anyone writes the AASA.
India-specific considerations
For teams sending from the Asia Pacific (Mumbai) Region, the redirect domain and its CDN origin have to sit in the same AWS Region as the SES identity - the Developer Guide requires the CNAME or CDN origin to point at the tracking domain for that Region, and SES recommends a separate subdomain per Region you send from. A single global tracking subdomain in front of multi-Region sending will not verify cleanly.
The verification files themselves are public by design and carry an Apple Team ID, a bundle identifier, a package name and a signing certificate fingerprint - no personal data - so they sit outside the scope of the Digital Personal Data Protection Act 2023. The click event stream is a different matter. It is recipient-level by construction, and if you are already applying consent controls to SES tracking, extending deep links does not change that obligation. Our note on the SES tracking override and bulk send consent limit covers where the per-recipient override actually applies.
Two adjacent constraints worth checking in the same sprint: the Android 17 targetSdk 37 adaptive layout migration, because an intent filter change means a release anyway and you may as well carry the API level with it, and ACM email validation ending in 2027, which touches the certificate on the same redirect domain.
What is still unknown
AWS has not published a way to read back which links in a sent message carried a valid ses:custom-path, so there is no server-side confirmation that the attribute was accepted rather than silently dropped. The only signal available today is behavioural: send a test, tap the link on a device with the app installed, and see whether the app opens.
Nor has AWS documented interaction with SES templates. The Developer Guide's warnings about template handling cover the {{ses:openTracker}} placeholder, where more than one instance returns a 400 BadRequestException; it says nothing about whether ses:custom-path survives template rendering intact. Test it in your template pipeline before assuming it does.
The real cost here is not the attribute. It is the release train.
FAQ
How eCorpIT can help
We build and ship iOS and Android applications, and this change lands squarely on the boundary between the email platform and the app release. If you need the verification files hosted correctly on a CloudFront-fronted SES redirect domain, the intent filter carried into a release build, and the adb verification checked before rollout, our custom mobile app development team does exactly that work. eCorpIT is CMMI Level 5, MSME Certified and ISO 27001:2022 certified. Tell us what you send and what your app expects at our contact page.
References
Last updated: 23 August 2026.