Callbacks for mobile and desktop apps
A Mobile or desktop app receives the member back from STX at a callback URL (redirect URI). STX accepts three styles for this application type. Each works with the same flow, and an app can register more than one.
| Style | Registered callback | What you host | If the app is not installed |
|---|---|---|---|
| Universal links (iOS) / App Links (Android) | https://example.com/stx/callback |
apple-app-site-association and assetlinks.json on your domain |
The link opens your website |
| Custom scheme | com.example.app:/callback |
Nothing | Nothing handles the link |
| Loopback | http://127.0.0.1/callback (or localhost, [::1]) |
Nothing; the app listens on the device | Not applicable: the app itself is listening |
What every style has in common
Section titled “What every style has in common”- Authorization code with PKCE, no client secret. A Mobile or desktop app has no
client_secretand cannot useclient_credentials. It sends itsclient_idin the token request and proves itself with the PKCE verifier. The requests themselves are on Authorization code flow; read the endpoints from discovery. - Exact match. The
redirect_uriyou send must match a callback registered for your app in STX Apps (apps.stxapp.io), byte for byte, at both the authorize and token steps. The one exception is loopback, below, which matches on any port. A callback has no fragment. - The callback carries the result in its query.
codeandstateon Allow,errorandstateotherwise. Checkstatebefore you exchange the code. See Receive the code.
In the examples below, authorizeURL is the authorize request from step 2 of the flow, with redirect_uri set to the registered callback shown beside each example.
Universal links and App Links
Section titled “Universal links and App Links”An ordinary https URL on a domain you control. You publish a file on that domain naming your app, and the operating system then opens your app for that URL instead of the browser. Without the app installed, the same URL loads in the browser, so put a page there.
Registered callback: https://example.com/stx/callback
Add the associated domains entitlement. applinks lets iOS open the app for the URL; webcredentials is what ASWebAuthenticationSession checks for an https callback.
<key>com.apple.developer.associated-domains</key><array> <string>applinks:example.com</string> <string>webcredentials:example.com</string></array>Serve this at https://example.com/.well-known/apple-app-site-association, as application/json, with no redirect. ABCDE12345 is your Apple team ID.
{ "applinks": { "details": [ { "appIDs": ["ABCDE12345.com.example.app"], "components": [{ "/": "/stx/callback" }] } ] }, "webcredentials": { "apps": ["ABCDE12345.com.example.app"] }}On iOS 17.4 and macOS 14.4 and later, ASWebAuthenticationSession takes an https callback and returns the URL to your completion handler:
import AuthenticationServicesimport UIKit
final class STXConnect: NSObject, ASWebAuthenticationPresentationContextProviding { private let window: UIWindow private var session: ASWebAuthenticationSession?
init(window: UIWindow) { self.window = window }
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { window }
func connect(authorizeURL: URL) { let session = ASWebAuthenticationSession( url: authorizeURL, callback: .https(host: "example.com", path: "/stx/callback") ) { callbackURL, error in guard let callbackURL, let query = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?.queryItems else { return } let code = query.first { $0.name == "code" }?.value let state = query.first { $0.name == "state" }?.value // Check state, then exchange code with your code_verifier. } session.presentationContextProvider = self self.session = session session.start() }}When the link opens the app from outside a session, it arrives in SwiftUI’s .onOpenURL (or scene(_:continue:) in a scene delegate):
ContentView() .onOpenURL { url in guard url.host == "example.com", url.path == "/stx/callback" else { return } // Read code and state from url, as above. }Android
Section titled “Android”Declare an autoVerify intent filter for the URL on the activity that handles the callback:
<activity android:name=".StxCallbackActivity" android:exported="true" android:launchMode="singleTask"> <intent-filter android:autoVerify="true"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="https" android:host="example.com" android:path="/stx/callback" /> </intent-filter></activity>Serve this at https://example.com/.well-known/assetlinks.json, with your package name and the SHA-256 fingerprint of your signing certificate:
[ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.example.app", "sha256_cert_fingerprints": [ "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5" ] } }]Open the authorize request in a Custom Tab (androidx.browser:browser), and read the callback in the activity:
fun connect(context: Context, authorizeUrl: Uri) { CustomTabsIntent.Builder().build().launchUrl(context, authorizeUrl)}
class StxCallbackActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) handle(intent) }
override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) handle(intent) }
private fun handle(intent: Intent) { val uri = intent.data ?: return val code = uri.getQueryParameter("code") val state = uri.getQueryParameter("state") // Check state, then exchange code with your code_verifier. }}Custom scheme
Section titled “Custom scheme”A scheme only your app claims, in reverse-domain form: it must contain a dot, such as com.example.app. A scheme without a dot (myapp://callback) and web schemes such as http, https, javascript or data are refused. There is nothing to host.
Registered callback: com.example.app:/callback
ASWebAuthenticationSession with callbackURLScheme catches the redirect to the scheme. The app does not need to declare the scheme in Info.plist for this.
import AuthenticationServicesimport UIKit
final class STXConnect: NSObject, ASWebAuthenticationPresentationContextProviding { private let window: UIWindow private var session: ASWebAuthenticationSession?
init(window: UIWindow) { self.window = window }
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { window }
func connect(authorizeURL: URL) { let session = ASWebAuthenticationSession( url: authorizeURL, callbackURLScheme: "com.example.app" ) { callbackURL, error in guard let callbackURL, let query = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?.queryItems else { return } let code = query.first { $0.name == "code" }?.value let state = query.first { $0.name == "state" }?.value // Check state, then exchange code with your code_verifier. } session.presentationContextProvider = self self.session = session session.start() }}Android
Section titled “Android”Declare the scheme on the activity that handles the callback:
<activity android:name=".StxCallbackActivity" android:exported="true" android:launchMode="singleTask"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="com.example.app" /> </intent-filter></activity>Open the authorize request in a Custom Tab and read the callback exactly as in the App Links example above: connect() and StxCallbackActivity are the same, and intent.data is com.example.app:/callback?code=…&state=….
Loopback
Section titled “Loopback”The app listens on the device’s loopback interface and the browser redirects to it. This suits desktop apps and command-line tools, and local development of any app. Register the callback without a port: STX matches a loopback callback on any port, as long as the scheme, host, path and query are the same. Bind whichever port is free when the app runs.
Registered callback: http://127.0.0.1/callback
STX accepts http://localhost, http://127.0.0.1 and http://[::1] for a Mobile or desktop app in production as well as in development.
Desktop (Node.js)
Section titled “Desktop (Node.js)”A one-shot listener on a free port, using the open package to launch the browser:
import http from 'node:http'import open from 'open'
const server = http.createServer((req, res) => { const url = new URL(req.url, 'http://127.0.0.1') if (url.pathname !== '/callback') return res.writeHead(404).end()
const code = url.searchParams.get('code') const state = url.searchParams.get('state') res.end('Connected. You can close this window.') server.close() // Check state, then exchange code with your code_verifier.})
server.listen(0, '127.0.0.1', () => { // For example http://127.0.0.1:53121/callback const redirectUri = `http://127.0.0.1:${server.address().port}/callback` // buildAuthorizeUrl: your authorize request, with this redirect_uri. open(buildAuthorizeUrl(redirectUri))})Send the same redirect_uri, port included, to the token endpoint.

