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¶
- The ACS receives the AReq and decrypts the Device Information.
- The ACS creates a permission with the
app2appintent, settingplatformandapp_callback_urifrom the AReq data. - The ACS calls
/app-authorizewith3ds_infocarrying the allowlisted Device Information fields (see the table below). - On success, the ACS presents the app-switch using the returned universal link.
- 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:
A device that does not match, or is not currently reachable via app2app, yields a 403:
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
nullas a placeholder for a missing value. - Send a flat object.
3ds_infois the flat set of allowlisted key/value pairs, not the fullDD/DPNA/SWenvelope 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_infofrom 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 ornull. - Identifiers are uppercase and matched case-sensitively.
c001is not the same key asC001and will be silently dropped as unrecognised. Send identifiers exactly as EMVCo defines them. device_infoand3ds_infoare mutually exclusive. Sending both on the same request is a400, not a merge of the two.- A
403is 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.