Skip to main content

Deep Links

Wallet flows start from URIs — QR codes render them, and other apps or web pages open them as deep links. The SDK handles two schemes:

SchemeFlowExample
openid-credential-offer://Issuanceopenid-credential-offer://?credential_offer_uri=https%3A%2F%2F...
openid4vp://Presentationopenid4vp://?request_uri=https%3A%2F%2F...

WalletSdk.handleLink classifies an inbound URI and, for recognized links, performs the intake work (offer normalization, presentation resolution and matching) so your app only decides which screen to show.

Dispatching a URI​

final result = await sdk.handleLink(
HandleLinkInput(
uri: inboundUri, // from a QR scan, app link, or paste field
walletExternalId: apiWalletId,
),
);

switch (result) {
case HandleLinkCredentialOffer(:final normalizedOfferUrl):
openOfferScreen(normalizedOfferUrl); // → acceptOffer

case HandleLinkPresentation prep:
openPresentationScreen(prep); // → present / presentation.start

case HandleLinkExpired():
showMessage('This request has expired.');

case HandleLinkUnsupported(:final message):
showMessage(message);

case HandleLinkFailed(:final message):
showMessage(message);
}

Result types​

HandleLinkCredentialOffer​

The URI is a credential offer. normalizedOfferUrl is ready to pass to acceptOffer as the offer value.

HandleLinkPresentation​

The URI is an OpenID4VP request. The SDK has already resolved it and matched stored credentials:

FieldDescription
preparedPresentationPreparedPresentation — resolved request, definition, verifier name, trust verdict
matchedCredentialsStored credentials satisfying the definition
requestedCredentialRawTypesCredential types the verifier asked for
originalPresentationUrlThe original inbound URL
presentationResponseUri / presentationDefinitionUriEndpoint details from the request
queryStateOpenID4VP state value
openId4VpClientMetadataVerifier client_metadata
resolvedVerifierMemberIdTrust-network member id, when resolvable

Feed these into the session API:

case HandleLinkPresentation prep:
final session = await sdk.presentation!.start(
StartPresentationRequest(
walletExternalId: apiWalletId,
presentationOfferUrl: prep.preparedPresentation.resolvedPresentationRequest,
verifier: verifierFrom(prep.preparedPresentation),
prepared: prep.preparedPresentation,
did: holderDid,
originalPresentationUrl: prep.originalPresentationUrl,
verifierMemberId: prep.resolvedVerifierMemberId,
),
);
final result = await session.present();

Terminal results​

  • HandleLinkExpired — the presentation request has expired; route to an expiry screen.
  • HandleLinkUnsupported — the URI is not a wallet link; show the message.
  • HandleLinkFailed — a recognized link failed during intake; show the message.

Classifying without dispatching​

To validate scanner input before running intake (e.g. enabling a "Continue" button), use WalletUriClassifier:

switch (WalletUriClassifier.classify(scannedText)) {
case WalletLinkKind.credentialOffer:
case WalletLinkKind.openId4Vp:
enableContinue();
case WalletLinkKind.unsupported:
showHint('Not a wallet QR code');
}

Platform registration​

Register the schemes so the OS delivers links to your app.

Android — AndroidManifest.xml​

<activity android:name=".MainActivity" ...>
<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="openid-credential-offer" />
</intent-filter>
<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="openid4vp" />
</intent-filter>
</activity>

iOS — Info.plist​

<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>openid-credential-offer</string>
<string>openid4vp</string>
</array>
</dict>
</array>

Listening in Flutter​

Use a deep-link plugin such as app_links to receive URIs and forward them to handleLink:

final appLinks = AppLinks();

// Cold start.
final initial = await appLinks.getInitialLink();
if (initial != null) await dispatch(initial);

// While running.
appLinks.uriLinkStream.listen((uri) => dispatch(uri));

Future<void> dispatch(Uri uri) async {
final result = await sdk.handleLink(
HandleLinkInput(uri: uri.toString(), walletExternalId: apiWalletId),
);
// switch on result ...
}
QR codes and deep links are the same input

A QR scanner yields the same URI a deep link delivers. Route both through one dispatcher so scan, deep link, and paste behave identically.