# 3DS app2app ACS integration

This page describes how a 3-D Secure Access Control Server (ACS) uses the `3ds_info` parameter on
`/app-authorize` to offer an app2app app-switch instead of falling back to a browser challenge.

This is intended as a **supplement** to the App2App flow already described in [App2App](https://docs.stoe.no/bankid-with-biometrics/advanced-topics/app2app-app-native.md).

> **Not the same `C001`**
>
> The identifiers in this page (`C001`, `D001`, and so on) are **EMVCo Device Information data
> elements**, defined by the 3-D Secure specification. They are unrelated to the BankID client
> error codes of the same name used in the support troubleshooting guide. If you land here from
> a search for an error code, this is probably not the page you want.

## Audience

This page is for the card issuer's **ACS**, not the merchant's app. The ACS is the caller because
it is the party that receives and decrypts the EMV 3-D Secure Device Information from the AReq
message during the cardholder authentication request. The merchant app never sees this data and
cannot call this endpoint.

## Requirements

### Scopes

You will need a BankID client with the following scopes enabled:

- `permissions/client`
- `permissions/app2app`
- `permissions/ciba`

Include all three scopes in the access token used to create the mixed-intent permission. The token used for the required `/bc-authorize` fallback must also include `permissions/ciba`.

### Intents

You must create a permission with the following intents: `intents: ["app2app", "ciba"]`

### `/bc-authorize` fallback

A `403 access_denied` with `error_description: "No matching device found"` from `/app-authorize`
means App2App has not started and is a normal, expected outcome. Some
devices and platforms cannot use app2app, and the contract is designed around that. When it
happens, fall back to the [CIBA](https://docs.stoe.no/bankid-with-biometrics/flows/ciba.md) flow via `/bc-authorize`. This fallback is
**required**, not optional: it is what lets game consoles, smart TVs, and any other device we
cannot reach via app2app still complete authentication, by sending a push message to the BankID
app on a usable device instead of attempting an app-switch.

Reuse the same still-valid, unconsumed
permission, send its `permissionToken` as `login_hint_token` and its saved
`bindingMessage` as `binding_message` in the `/bc-authorize` request, then poll the `auth_req_id`
returned by CIBA.

### Use `3ds_info` instead of `device_info`

The rest of this page describes how to use `3ds_info` instead of `device_info`.

## The `3ds_info` allowlist

The server accepts a flat JSON object of EMVCo Device Information identifiers, encoded as
base64url and sent as the `3ds_info` form value. Only the identifiers below are accepted; any
other key is dropped silently before the request is processed further.

Use this allowlist when selecting keys from the SDK payload. Retain only keys in this set
and omit everything else. Non-allowlisted fields may contain additional personal data and must not
be sent. The server silently ignores non-allowlisted keys if they are received, but clients must
filter them before building `3ds_info`.

Java:

```java
Set<String> allowedFields = Set.of(
    "C001", "D001", "C002", "D002", "C003", "D003", "C004", "C008", "D008",
    "A050", "A044", "C005", "D005", "D027", "C006", "D006", "C013", "D013",
    "C016", "DV", "C010", "D029"
);
```

C#:

```csharp
var allowedFields = new HashSet<string>(StringComparer.Ordinal)
{
    "C001", "D001", "C002", "D002", "C003", "D003", "C004", "C008", "D008",
    "A050", "A044", "C005", "D005", "D027", "C006", "D006", "C013", "D013",
    "C016", "DV", "C010", "D029",
};
```

Send every available allowlisted field your 3DS SDK exposes. Omit an unavailable allowlisted field
by leaving the key out. Never send an empty string or `null` as a placeholder. See
[pitfalls](https://docs.stoe.no/bankid-with-biometrics/advanced-topics/3ds-app2app-acs.md#pitfalls) below.

### Tier 1 — device identity

These describe the device itself and carry the most weight when we decide whether an app-switch
can be offered. Send every available allowlisted field your SDK provides.

| Identifier | Description                                                                           | Typical source in the 3DS SDK                                                                  |
| ---------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `C001`     | Cardholder device operating system name (for example `Android`, `iOS`).               | Reported directly by the 3DS SDK as the device operating system name.                          |
| `D001`     | Same as `C001`, used by SDKs that report Device Information as a duplicated C/D pair. | Reported alongside `C001` on split-SDK integrations; used as a fallback when `C001` is absent. |
| `C002`     | Device model identifier, for example a marketing name or internal model code.         | Reported by the SDK as the device model or marketing name.                                     |
| `D002`     | Same as `C002`, duplicated C/D pair.                                                  | Reported alongside `C002` on split-SDK integrations.                                           |
| `C003`     | Operating system name and version string.                                             | Reported by the SDK as the OS version string.                                                  |
| `D003`     | Same as `C003`, duplicated C/D pair.                                                  | Reported alongside `C003` on split-SDK integrations.                                           |
| `C004`     | Device locale identifier.                                                             | Reported by the SDK from the device's configured locale.                                       |
| `C008`     | Device screen resolution.                                                             | Reported by the SDK as the screen resolution string.                                           |
| `D008`     | Same as `C008`, duplicated C/D pair.                                                  | Reported alongside `C008` on split-SDK integrations.                                           |

### Tier 2 — manufacturer repair

| Identifier | Description                                                                           | Typical source in the 3DS SDK                      |
| ---------- | ------------------------------------------------------------------------------------- | -------------------------------------------------- |
| `A050`     | Device manufacturer name.                                                             | Reported by the SDK from platform device metadata. |
| `A044`     | Device brand name, which can differ from the manufacturer on some Android OEM builds. | Reported by the SDK from platform device metadata. |

### Tier 3 — context

| Identifier | Description                                             | Typical source in the 3DS SDK                                           |
| ---------- | ------------------------------------------------------- | ----------------------------------------------------------------------- |
| `C005`     | Device timezone offset or identifier.                   | Reported by the SDK from the device's configured timezone.              |
| `D005`     | Same as `C005`, duplicated C/D pair.                    | Reported alongside `C005` on split-SDK integrations.                    |
| `D027`     | Human-readable timezone name (for example a city list). | Reported by the SDK where the platform exposes a display timezone name. |
| `C006`     | Device timezone offset, expressed in minutes from UTC.  | Reported by the SDK from the device's configured timezone offset.       |
| `D006`     | Same as `C006`, duplicated C/D pair.                    | Reported alongside `C006` on split-SDK integrations.                    |

### Tier 4 — provenance

| Identifier | Description                            | Typical source in the 3DS SDK                                                    |
| ---------- | -------------------------------------- | -------------------------------------------------------------------------------- |
| `C013`     | 3DS SDK application identifier.        | Assigned by the ACS or DS to the 3DS Requestor App and reported back by the SDK. |
| `D013`     | Same as `C013`, duplicated C/D pair.   | Reported alongside `C013` on split-SDK integrations.                             |
| `C016`     | 3DS SDK reference number.              | Provided by the SDK vendor and reported by the SDK.                              |
| `DV`       | 3DS Device Information version number. | Reported by the SDK as the version of its Device Information payload format.     |

### Tier 5 — local network

| Identifier | Description                                               | Typical source in the 3DS SDK                                  |
| ---------- | --------------------------------------------------------- | -------------------------------------------------------------- |
| `C010`     | IP address of the cardholder device on its local network. | Reported by the SDK from the device's local network interface. |
| `D029`     | Same as `C010`, duplicated C/D pair.                      | Reported alongside `C010` on split-SDK integrations.           |

## Worked example

Decoded `3ds_info` JSON object:

```json
{
  "C001": "Android",
  "C002": "SM-S908B",
  "C003": "Android BAKLAVA 16 API 36",
  "C006": "-120",
  "C008": "1080x2340",
  "A044": "samsung"
}
```

The same value, base64url-encoded:

```text
eyJDMDAxIjoiQW5kcm9pZCIsIkMwMDIiOiJTTS1TOTA4QiIsIkMwMDMiOiJBbmRyb2lkIEJBS0xBVkEgMTYgQVBJIDM2IiwiQzAwNiI6Ii0xMjAiLCJDMDA4IjoiMTA4MHgyMzQwIiwiQTA0NCI6InNhbXN1bmcifQ
```

Full form POST to `/app-authorize`:

```text
POST /app-authorize HTTP/1.1
Content-Type: application/x-www-form-urlencoded

login_hint_token=<login_hint_token>&scope=openid&3ds_info=eyJDMDAxIjoiQW5kcm9pZCIsIkMwMDIiOiJTTS1TOTA4QiIsIkMwMDMiOiJBbmRyb2lkIEJBS0xBVkEgMTYgQVBJIDM2IiwiQzAwNiI6Ii0xMjAiLCJDMDA4IjoiMTA4MHgyMzQwIiwiQTA0NCI6InNhbXN1bmcifQ
```

A registered device that can be reached via app2app yields a `200` with an app-switch link:

```json
{
  "authorize_uri": "https://app.bankid.no/authorize?..."
}
```

A device that does not match, or is not currently reachable via app2app, yields a `403`:

```json
{
  "error": "access_denied",
  "error_description": "No matching device found"
}
```

Treat that `403` as the expected signal to fall back to `/bc-authorize`, not as an integration bug.

## 400 versus 403

| Condition                                                                       | Result                             |
| ------------------------------------------------------------------------------- | ---------------------------------- |
| `3ds_info` is not valid base64url, or does not decode to valid JSON             | `400 invalid_request`              |
| Both `device_info` and `3ds_info` are present, or neither is present            | `400 invalid_request`              |
| Unknown or non-allowlisted keys are present                                     | Dropped silently, request proceeds |
| A value is an unexpected shape or over-long                                     | Dropped silently, request proceeds |
| The device information does not match a device registered for app2app           | `403 access_denied`                |
| The login hint does not identify a user enrolled for Substantial authentication | `400 unknown_user_id`              |

A well-formed `3ds_info` request returns `400 unknown_user_id` when the login hint does not
identify a user enrolled for Substantial authentication. Treat this as a terminal outcome. Do not
fall back to `/bc-authorize`; that fallback is for `403 access_denied` with
`error_description: "No matching device found"`, which means app-switch is unavailable for the identified user
from this device.

## Pitfalls

- **Pass values through verbatim.** Do not normalise casing, trim whitespace, or reformat any
  value before sending it. Normalising discards information we cannot recover.
- **Send every available allowlisted field your SDK exposes.** Omit unavailable allowlisted fields
  by leaving their keys out of the object entirely. Never send an empty string or `null` as a
  placeholder for a missing value.
- **Send a flat object.** `3ds_info` is the flat set of allowlisted key/value pairs, not the full
  `DD`/`DPNA`/`SW` envelope some 3DS SDKs produce internally.
- **An unrecognised key will not fail your request.** A key outside the allowlist is dropped
  rather than rejected, so a new field from an SDK upgrade cannot break your integration. That
  is a robustness guarantee, not permission to send everything: build `3ds_info` from the
  allowlist. Non-allowlisted fields may contain additional personal data and must not be sent.
  Do not rely on us to discard data you should not have sent. A field you cannot populate is
  omitted entirely, never sent as an empty string or `null`.
- **Identifiers are uppercase and matched case-sensitively.** `c001` is not the same key as
  `C001` and will be silently dropped as unrecognised. Send identifiers exactly as EMVCo defines
  them.
- **`device_info` and `3ds_info` are mutually exclusive.** Sending both on the same request is a
  `400`, not a merge of the two.
- **A device-match `403` is a normal, expected outcome.** The CIBA fallback described above is
  required rather than optional. Build it into your integration from the start instead of
  treating it as an edge case.

## Wire versus contract

| Level    | Rule                                                       |
| -------- | ---------------------------------------------------------- |
| Wire     | Every field is optional. A missing field is never a `400`. |
| Contract | Send every available allowlisted field your SDK exposes.   |

The parser is lenient so that a partial payload degrades gracefully in production. That leniency
is not permission to send less than your SDK gives you. The contract is that you send the
full set of allowlisted fields you have available, every time.

## Not a fraud control

This check exists to decide whether the app-switch can be offered at all. It is not a fraud
control, and it does not replace one. Fraud detection and risk scoring remain the responsibility
of the ACS engine.
