Opening the Return Redirect 4 min read
Once the customer has authenticated and made their decision, you complete the interaction by calling doConfirm or doFail on the Headless Heimdall Auth Server. The response returns the URL to send the customer back to the TPP. How your app opens that URL is security-critical — get it wrong and you break app-to-app redirection or expose the customer to credential theft.
The URL returned by doConfirm / doFail
After you post the customer's decision, Headless Heimdall responds with an HTTP 303 whose Location header carries the redirect URI — the URL that returns the authorization result (or error) to the TPP. Your app is responsible for opening it:
HTTP/1.1 303 See Other
Location: https://tpp.example.com/callback?code=fbe03604-baf2-4220-b7dd-05b14de19e5c&state=d2fe5e2c-77cd-4788-b0ef-7cf0fc8a3e54&iss=https://auth1.altareq1.sandbox.apihub.openfinance.ae This URL is the TPP's registered redirect_uri. On mobile it may be a verified deep link into the TPP's app, so opening it correctly can hand the customer straight back into the app they started in. See the endpoint references for doConfirm and doFail, and the Consent Journey API Guide for the full flow.
This page covers the outbound redirect — sending the customer from your app back to the TPP. For the inbound direction — how the customer's arrival at your Authorization Endpoint opens your own app — see Authorization Endpoint.
Prefer an external user-agent, then fall back
When your customer is inside your native app and you need to open the return redirect, you MUST open it in an external user-agent — never an embedded WebView. Two launch modes satisfy this, and you SHOULD try them in order:
- External application mode (preferred). Hand the URL to the operating system. Where the TPP's mobile app is installed and the URL is a verified Universal Link (iOS) or App Link (Android), the OS opens that app directly — this is app-to-app redirection. If the app is not installed, the OS opens the device's system browser. Both are compliant external user-agents.
- Platform default mode (fallback). If the external-application launch throws or returns
false, catch the error and retry in platform-default mode. ForhttpsURLs this resolves to an in-app browser tab — Chrome Custom Tabs on Android,SFSafariViewControlleron iOS — which is still an acceptable external user-agent.
External-application mode is tried first because it is the only mode that can hand the customer straight into the TPP's mobile app (app-to-app), giving the smoothest, most trusted experience. The in-app browser tab reached by platform-default mode renders the URL in a tab rather than handing off to the app, so app-to-app generally will not trigger — which is why it is the fallback, not the default.
Try external-application, catch the error, fall back
The pattern is the same across native stacks: attempt to launch in external-application mode, and on failure fall back to the platform default. The example below uses Flutter's url_launcher; the same shape applies to native iOS, native Android, and React Native.
import 'package:url_launcher/url_launcher.dart';
/// Opens an outbound Open Finance redirect URL from within the app.
Future<void> openRedirect(Uri redirectUri) async {
try {
// 1. Prefer an external user-agent. Where the TPP's mobile app is installed
// and the URL is a verified Universal Link / App Link, the OS opens
// that app directly (app-to-app). Otherwise it opens the system browser.
final launched = await launchUrl(
redirectUri,
mode: LaunchMode.externalApplication,
);
if (!launched) {
throw Exception('externalApplication launch returned false');
}
} catch (_) {
// 2. Fall back to the platform default. For https URLs this resolves to an
// in-app browser tab (Chrome Custom Tabs / SFSafariViewController) —
// still a compliant external user-agent.
// NEVER fall back to LaunchMode.inAppWebView (an embedded WebView).
await launchUrl(
redirectUri,
mode: LaunchMode.platformDefault,
);
}
}| Platform | External application (try first) | Platform default / in-app tab (fallback) |
|---|---|---|
| iOS | UIApplication.open(_:) | SFSafariViewController |
| Android | Intent.ACTION_VIEW to the system default | Chrome Custom Tabs |
| Flutter | LaunchMode.externalApplication | LaunchMode.platformDefault |
Never use an embedded WebView
The redirect MUST NOT be opened in an embedded WebView (for example WKWebView, Android WebView, or LaunchMode.inAppWebView). This is a hard requirement of RFC 8252 (OAuth 2.0 for Native Apps), which the FAPI security profile and UAE Open Finance are built on.
A host app can read everything inside its own embedded WebView — entered credentials, cookies, and session tokens. Embedded WebViews also break verified deep linking, so app-to-app redirection cannot occur and the customer is forced to re-authenticate from scratch. If a launch fails, fall back to the in-app browser tab (platform-default mode) — never to an embedded WebView.
Web and desktop: follow the 303 as a full-page redirect
If the customer authenticated in a web browser — your desktop or mobile web channel — instead of your native app, opening the return redirect is simpler. Just follow the doConfirm / doFail 303 as a normal full-page browser redirect back to the TPP's redirect_uri.
- Follow the
Locationheader at the top level of the browser — a standard303redirect. Do not wrap the return in an<iframe>or embedded frame. - Desktop cross-device journeys — where the customer started on a desktop browser and authenticated on their phone — complete on your Authorization Endpoint page, which polls for completion and then performs this redirect. See Authorization Endpoint — Desktop browser behaviour.
UK Open Banking's redirection model is domain-to-domain and full-page in both directions — the outbound screen returns the customer from the LFI domain to the TPP domain — and never embeds one party's pages inside the other. Desktop customers who authenticate on a second device are handled by decoupled / QR-code hand-off on your Authorization Endpoint.
