# Callbacks for mobile and desktop apps

> The three callback styles a Mobile or desktop app can register with STX (universal links and App Links, a custom scheme, and loopback), with a minimal iOS, Android and desktop example of each.

Source: https://docs.stxapp.io/apps/mobile-and-desktop-callbacks/

> **Availability.** Available on US demo now. Live access is coming soon in the US and Ontario. See [Environments](https://docs.stxapp.io/environments/).

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](/apps/overview/#application-types). 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

- **Authorization code with PKCE, no client secret.** A Mobile or desktop app has no `client_secret` and cannot use `client_credentials`. It sends its `client_id` in the token request and proves itself with the PKCE verifier. The requests themselves are on [Authorization code flow](/oauth/authorization-flow/); read the endpoints from [discovery](/oauth/discovery-and-errors/#discovery).
- **Exact match.** The `redirect_uri` you send must match a callback registered for your app in [STX Apps (apps.stxapp.io)](https://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.** `code` and `state` on **Allow**, `error` and `state` otherwise. Check `state` before you exchange the code. See [Receive the code](/oauth/authorization-flow/#3-receive-the-code-at-your-redirect-uri).

In the examples below, `authorizeURL` is the authorize request from [step 2 of the flow](/oauth/authorization-flow/#2-redirect-the-member-to-oauthauthorize), with `redirect_uri` set to the registered callback shown beside each example.

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

### iOS

Add the associated domains entitlement. `applinks` lets iOS open the app for the URL; `webcredentials` is what `ASWebAuthenticationSession` checks for an `https` callback.

```xml
<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.

```json
{
  "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:

```swift
import AuthenticationServices
import 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):

```swift
ContentView()
  .onOpenURL { url in
    guard url.host == "example.com", url.path == "/stx/callback" else { return }
    // Read code and state from url, as above.
  }
```

### Android

Declare an `autoVerify` intent filter for the URL on the activity that handles the callback:

```xml
<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:

```json
[
  {
    "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:

```kotlin
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

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`

### iOS

`ASWebAuthenticationSession` with `callbackURLScheme` catches the redirect to the scheme. The app does not need to declare the scheme in `Info.plist` for this.

```swift
import AuthenticationServices
import 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

Declare the scheme on the activity that handles the callback:

```xml
<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

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)

A one-shot listener on a free port, using the [`open`](https://www.npmjs.com/package/open) package to launch the browser:

```js
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.
