Skip to content

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.

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.

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.

The sequence

  1. The ACS receives the AReq and decrypts the Device Information.
  2. The ACS creates a permission with the app2app intent, setting platform and app_callback_uri from the AReq data.
  3. The ACS calls /app-authorize with 3ds_info carrying the allowlisted Device Information fields (see the table below).
  4. On success, the ACS presents the app-switch using the returned universal link.
  5. On 403, the ACS falls back to /bc-authorize.

The 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:

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

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

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

The same value, base64url-encoded:

eyJDMDAxIjoiQW5kcm9pZCIsIkMwMDIiOiJTTS1TOTA4QiIsIkMwMDMiOiJBbmRyb2lkIEJBS0xBVkEgMTYgQVBJIDM2IiwiQzAwNiI6Ii0xMjAiLCJDMDA4IjoiMTA4MHgyMzQwIiwiQTA0NCI6InNhbXN1bmcifQ

Full form POST to /app-authorize:

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:

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

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

{
  "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 only for 403 access_denied, which means app-switch is unavailable for the identified user.

/bc-authorize fallback

A 403 from /app-authorize is a normal, expected outcome rather than an error condition. Some devices and platforms cannot use app2app, and the contract is designed around that. When it happens, fall back to the CIBA 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 instead of attempting an app-switch.

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 403 is a normal, expected outcome, not an error. 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.