> For the complete documentation index, see [llms.txt](https://docs.payments.thalescloud.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.payments.thalescloud.io/push-provisioning/implement-push-provisioning/implement-view-and-control/in-app-authentication-for-visa-ctf.md).

# In-app authentication for Visa CTF

{% hint style="danger" %}
Tokenization service is required to use the Thales D1 SDK for the in-app authentication flow.
{% endhint %}

Visa Cloud Token Framework (CTF) is a Visa framework designed to increase trust in e-commerce digital cards after Tokenization.

CTF defines two flows: device binding and cardholder verification.

These flows let the merchant increase trust in a digital card by triggering a cardholder step-up authentication flow with the Issuer.

For more details about the full flows and Visa Cloud Token Framework (CTF), see the section [Visa Cloud token framework](/tokenization/implement-tokenization/post-tokenization-requests/visa-cloud-token-framework-ctf.md) on the Tokenization documentation.

The in-app authentication flow for Visa CTF is very similar to the flow described in [In app authentication](/push-provisioning/implement-push-provisioning/implement-view-and-control/in-app-authentication.md).

Steps \[01] to \[10] are essentially the same.

The main difference is the payload format, shown in the following table:

<table><thead><tr><th>Device binding - payload</th><th>Cardholder verification - payload</th></tr></thead><tbody><tr><td><pre><code>{
  "panReferenceID": "V-3815023863409817870482",
  "tokenRequestorID": "42301999123",
  "tokenReferenceID": "DNITHE381502386342002358",
  "panLast4": "1234",
  "deviceID": "DEiOiJBMjU2R_0NNS1-ciLCJiI",
  "walletAccountID": "AiOiJBMjU-2_R0NNS1ciLCJiI6",
  "deviceIndex": "1",
  "reasonCode": "TOKEN_DEVICE_BINDING"
}
</code></pre></td><td><pre><code>{
  "panReferenceID": "V-3815023863409817870482",
  "tokenRequestorID": "42301999123",
  "tokenReferenceID": "DNITHE381502386342002358",
  "panLast4": "1234",
  "deviceID": "null",
  "walletAccountID": "AiOiJBMjU-2_R0NNS1ciLCJiI6",
  "deviceIndex": "null",
  "reasonCode": "CARDHOLDER_STEPUP"
}
</code></pre></td></tr></tbody></table>

Compared with standard Tokenization authentication, the issuer application must inspect:

* **reasonCode**: identifies the reason for the CTF authentication.
  * `TOKEN_DEVICE_BINDING`: device binding flow.
  * `CARDHOLDER_STEPUP`: cardholder verification flow.
* **deviceIndex**: used for the device binding flow. This is the Visa device reference. In the D1 SDK, it maps to `bindingReference`.

When you refer to the sequence diagram in [In app authentication](/push-provisioning/implement-push-provisioning/implement-view-and-control/in-app-authentication.md), replace steps \[11] and \[12] with a new API call.

In this case, there is no digital card activation. Instead, the authentication result must be propagated to Visa VTS.

The D1 SDK exposes the following APIs so the issuer application can report the result:

1. For `TOKEN_DEVICE_BINDING`, call `DigitalCardService.approveBinding` with `digitalCardID` and `deviceIndex`.
2. For `CARDHOLDER_STEPUP`, call `DigitalCardService.approveCardholderVerification` with `digitalCardID`.

The D1 SDK also lets you:

1. Retrieve the list of bound devices as `deviceBindingList` when `getDigitalCardList()` is called. For details, see [View and control digital cards](/push-provisioning/implement-push-provisioning/implement-view-and-control/view-and-control-digital-cards.md).
2. Unbind a device by calling `DigitalCardService.unbindDevice`. See [Device unbind](/push-provisioning/implement-push-provisioning/implement-view-and-control/in-app-authentication-for-visa-ctf/unbind-a-device.md).

The following examples show how to parse the payload and approve a binding request.

{% hint style="info" %}
Note

The following fields mapping apply:

* `tokenReferenceID` = `digitalCardID`
* `deviceIndex` = `bindingReference`
  {% endhint %}

{% tabs %}
{% tab title="Android" %}

```kotlin
private const val VISA_STEPUP_RESULT_RESPONSE = "STEP_UP_RESPONSE"
private const val STEPUP_RESULT_APPROVED = "approved"
private const val STEPUP_RESULT_DECLINED = "declined"
private const val STEPUP_RESULT_FAILED = "failure"

val visaPayload = intent.getStringExtra(Intent.EXTRA_TEXT) ?: return

val payloadJson = try {
    val decodedPayload = String(Base64.decode(visaPayload, Base64.DEFAULT), Charsets.UTF_8)
    JSONObject(decodedPayload)
} catch (_: IllegalArgumentException) {
    // Invalid base64 payload
    return
} catch (_: JSONException) {
    // Invalid JSON payload
    return
}

val reasonCode = payloadJson.optString("reasonCode", "")
val digitalCardID = payloadJson.optString("tokenReferenceID", "") // tokenReferenceID -> digitalCardID
val bindingReference = payloadJson.optString("deviceIndex", "") // deviceIndex -> bindingReference

if ("TOKEN_DEVICE_BINDING" == reasonCode &&
    digitalCardID.isNotEmpty() &&
    bindingReference.isNotEmpty()) {

    d1Task.digitalCardService().approveBinding(
        digitalCardID,
        bindingReference,
        DigitalCardService.BindingApprovalReason.USER_DECISION,
        object : D1Task.Callback<Void> {
            override fun onSuccess(data: Void?) {
                sendResultToTokenRequestorApp(activity, null)
            }

            override fun onError(e: D1Exception) {
                sendResultToTokenRequestorApp(activity, e)
            }
        }
    )
} else {
    // Other flows (e.g., activation)
}

private fun sendResultToTokenRequestorApp(activity: Activity, exception: D1Exception?) {
    val result = Intent()
    // possible values: "approved", "declined", or "failure"
    val stepupResult = if (exception == null) STEPUP_RESULT_APPROVED else STEPUP_RESULT_FAILED
    result.putExtra(VISA_STEPUP_RESULT_RESPONSE, stepupResult)
    // applicable if issuer is using activation via authentication code
    activity.setResult(Activity.RESULT_OK, result)
    activity.finish()
}
```

```java
private static final String VISA_STEPUP_RESULT_RESPONSE = "STEP_UP_RESPONSE";
private static final String STEPUP_RESULT_APPROVED = "approved";
private static final String STEPUP_RESULT_DECLINED = "declined";
private static final String STEPUP_RESULT_FAILED = "failure";

String visaPayload = getIntent().getStringExtra(Intent.EXTRA_TEXT);
if (visaPayload == null) {
    return;
}

JSONObject payloadJson;
try {
    byte[] decodedPayloadBytes = Base64.decode(visaPayload, Base64.DEFAULT);
    String decodedPayload = new String(decodedPayloadBytes, StandardCharsets.UTF_8);
    payloadJson = new JSONObject(decodedPayload);
} catch (IllegalArgumentException | JSONException e) {
    // Invalid base64/JSON payload
    return;
}

String reasonCode = payloadJson.optString("reasonCode", "");
String digitalCardID = payloadJson.optString("tokenReferenceID", ""); // tokenReferenceID -> digitalCardID
String bindingReference = payloadJson.optString("deviceIndex", ""); // deviceIndex -> bindingReference

if ("TOKEN_DEVICE_BINDING".equals(reasonCode) &&
        !digitalCardID.isEmpty() &&
        !bindingReference.isEmpty()) {

    d1Task.digitalCardService().approveBinding(
            digitalCardID,
            bindingReference,
            DigitalCardService.BindingApprovalReason.USER_DECISION,
            new D1Task.Callback<Void>() {
                @Override
                public void onSuccess(Void data) {
                    sendResultToTokenRequestorApp(activity, null);
                }

                @Override
                public void onError(@NonNull D1Exception exception) {
                    sendResultToTokenRequestorApp(activity, exception);
                }
            });
} else {
    // Other flows (e.g., activation)
}

private void sendResultToTokenRequestorApp(Activity activity, D1Exception exception) {
    Intent result = new Intent ();
    // possible values: "approved", "declined", or "failure"
    String stepupResult = (exception == null) ? STEPUP_RESULT_APPROVED : STEPUP_RESULT_FAILED;
    result.putExtra(VISA_STEPUP_RESULT_RESPONSE, stepupResult);
    // applicable if issuer is using activation via authentication code
    activity.setResult(RESULT_OK, result);
    activity.finish();
}
```

{% endtab %}

{% tab title="iOS" %}

```swift
// To support app redirection from Wallet app.
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return false }

    let queryDict = components.queryItems?.reduce(into: [String: String]()) { dict, item in
        dict[item.name] = item.value
    } ?? [:]

    switch components.host {
    // Example service name that handles Visa app-to-app
    case "visaCTF":
        guard let wpCallback = queryDict["wpcallback"]?.removingPercentEncoding,
              let encodedPayload = queryDict["a2apayload"],
              let payloadData = Data(base64Encoded: encodedPayload.replacingOccurrences(of: " ", with: "+")),
              let json = try? JSONSerialization.jsonObject(with: payloadData) as? [String: Any],
              let reasonCode = json["reasonCode"] as? String else {
            return false
        }

        switch reasonCode {
        case "TOKEN_DEVICE_BINDING":
            guard let digitalCardID = json["tokenReferenceID"] as? String,
                  let bindingReference = json["deviceIndex"] as? String else {
                return false
            }

            Task {
                do {
                    try await d1Task.digitalCardService().approveBinding(
                        digitalCardID,
                        bindingReference: bindingReference,
                        reason: nil
                    )
                    sendResultToApp(wpCallback: wpCallback, stepupResult: "Approved")
                } catch {
                    sendResultToApp(wpCallback: wpCallback, stepupResult: "Failure")
                }
            }

        default:
            return false
        }

        return true
    default:
        return false
    }
}

private func sendResultToApp(wpCallback: String, stepupResult: String) {
    guard var components = URLComponents(string: wpCallback) else { return }

    var items = components.queryItems ?? []
    items.append(URLQueryItem(name: "stepupresponse", value: stepupResult))
    components.queryItems = items

    if let callbackURL = components.url {
        UIApplication.shared.open(callbackURL)
    }
}
```

{% endtab %}
{% endtabs %}

For a full access to the D1 SDK, please check [API reference](/push-provisioning/integrate-the-d1-sdk/api-reference.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.payments.thalescloud.io/push-provisioning/implement-push-provisioning/implement-view-and-control/in-app-authentication-for-visa-ctf.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
