Skip to content

How to test 3DS2 challenge flows with 3DS test cards

To test EMV 3-D Secure (3DS2) challenge flows, run sandbox test cards that force each authentication outcome in the browser and app channels, then match every authentication response to its challenge messages and results request (RReq). The check that catches the costliest defect is an authorisation sent only after the RReq, with the RReq authentication value.

Where challenge flow defects come from

A checkout that authorises before the results request passes every sandbox run: the challenge succeeds, the payment succeeds, and the defect sits in the timing and ordering of six messages. Three patterns reach production most often:

  • Authorisation on the wrong message. The checkout authorises on the authentication response (ARes) status C, or on the challenge response (CRes) posted through the browser. The final CRes carries no authenticationValue and no eci: in the EMV 3DS Core Specification, those fields travel in the results request (RReq) from the issuer's Access Control Server (ACS). An authorisation without them is unauthenticated, so the card scheme liability shift described on the EMV 3DS testing requirements page does not apply and fraud chargebacks stay with the merchant.
  • Timeouts treated as declines. The payer takes too long, the ACS closes the challenge, and the checkout shows a generic error. The payer presses pay again, a second authentication starts, and a slow payment service can place two authorisation holds. The payment idempotency guide covers the duplicate charge side.
  • A slow results handler. The ACS posts the final CRes to the browser only after it receives the results response (RRes). A 3DS Server that runs fraud scoring or the authorisation before answering the RReq can leave the payer in a frozen challenge window.

In the EEA, a payment that loses its authentication also loses strong customer authentication, so the issuer can decline it; the PSD2 SCA testing requirements page covers those rules, and the SCA exemptions guide covers the exemption requests that avoid a challenge.

The message sequence each test asserts

EMVCo publishes the specification versions on its EMV 3-D Secure page (checked on 2026-09-22), which lists specification bulletins for versions 2.2.0 to 2.3.1.1 and a 2.4.0 draft. The field names below are the Core Specification names; a 3DS Server vendor API may wrap them in its own request format.

StepMessagePathFields the test asserts
13DS MethodBrowser to ACS, hidden iframethreeDSMethodData with threeDSServerTransID and threeDSMethodNotificationURL
2AReq3DS Server to Directory Server (DS) to ACSdeviceChannel, messageCategory, threeDSCompInd, threeDSRequestorChallengeInd, notificationURL
3AResACS to DS to 3DS ServertransStatus, acsURL or acsSignedContent, acsChallengeMandated, acsTransID
4CReq and CResBrowser or 3DS SDK to ACS, repeated per challenge screenchallengeWindowSize, challengeCompletionInd
5RReqACS to DS to 3DS ServertransStatus, authenticationValue, eci, interactionCounter, challengeCancel
6RRes3DS Server to DS to ACSresultsStatus
7Final CResACS through the browser to notificationURLtransStatus Y or N only

Version 2.2.0 defines eight transStatus values: Y authenticated, N not authenticated, U could not be performed, A attempts processing, C challenge required, D decoupled authentication, R rejected, and I informational only. The final CRes can carry only Y or N.

Procedure

  1. Choose test cards by the outcome they force. Each 3DS Server or payment service sandbox publishes its own test cards, and they work only in that sandbox. The 3dsecure.io sandbox (checked on 2026-09-22) encodes the scenario in the last four digits: the first selects the message version (2 for 2.2.0, 3 for 2.3.1), the second the 3DS Method (0 included, 1 missing, 2 timeout), and the last two the ARes outcome, where 7x starts a challenge that passes (70), fails (71), or waits for manual input (72). Its published card 7000100911112070 runs a 2.2.0 challenge that returns Y in the RReq. Card numbers for authorisation and decline testing belong to the separate test card numbers guide.

  2. Run the 3DS Method and record its completion. Post threeDSMethodData to the threeDSMethodURL from the card range data, then set threeDSCompInd to Y when the notification arrives, N when it does not, and U when the card range has no method URL. The 3dsecure.io integration guide treats a notification not received within 10 seconds as failed. Use a last-four value with 2 in the second position to force that path.

  3. Send a browser AReq that requests a challenge. Value 04 of threeDSRequestorChallengeInd requests a challenge as a mandate. The request contains the card number in acctNumber, which is cardholder data: log it masked, even in a sandbox. Fields not relevant to the challenge are omitted.

JSON
{
  "messageType": "AReq",
  "messageVersion": "2.2.0",
  "messageCategory": "01",
  "deviceChannel": "02",
  "threeDSServerTransID": "8a880dc0-d2d2-4067-bcb1-b08d1690b26e",
  "threeDSCompInd": "Y",
  "threeDSRequestorChallengeInd": "04",
  "acctNumber": "7000100911112070",
  "purchaseAmount": "4250",
  "purchaseCurrency": "978",
  "purchaseExponent": "2",
  "purchaseDate": "20260922101500",
  "notificationURL": "https://shop.example.com/3ds/notify",
  "browserJavascriptEnabled": true,
  "browserLanguage": "en-GB",
  "browserTZ": "-60",
  "browserUserAgent": "Mozilla/5.0 (X11; Linux x86_64)"
}

purchaseAmount 4250 with exponent 2 and currency 978 is EUR 42.50. Keep this amount through authorisation; a changed basket needs a new authentication.

  1. Start the challenge from the ARes. A browser ARes with status C returns the acsURL.
JSON
{
  "messageType": "ARes",
  "messageVersion": "2.2.0",
  "threeDSServerTransID": "8a880dc0-d2d2-4067-bcb1-b08d1690b26e",
  "acsTransID": "d7c1ee99-9478-44a6-b1f2-391e29c6b340",
  "dsTransID": "f25084f0-5b16-4c0a-ae5d-b24808a95e4b",
  "transStatus": "C",
  "acsChallengeMandated": "Y",
  "authenticationType": "02",
  "acsURL": "https://acs.example.com/challenge"
}

The checkout renders an iframe and posts the CReq as a base64url-encoded JSON object in the creq form field, with optional threeDSSessionData that the ACS returns unchanged. Window size 05 is full screen.

Request
POST /challenge HTTP/1.1
Host: acs.example.com
Content-Type: application/x-www-form-urlencoded

creq=eyJ0aHJlZURTU2VydmVyVHJhbnNJRCI6IjhhODgwZGMwLWQyZDItNDA2Ny1iY2IxLWIwOGQxNjkwYjI2ZSIsImFjc1RyYW5zSUQiOiJkN2MxZWU5OS05NDc4LTQ0YTYtYjFmMi0zOTFlMjljNmIzNDAiLCJtZXNzYWdlVHlwZSI6IkNSZXEiLCJtZXNzYWdlVmVyc2lvbiI6IjIuMi4wIiwiY2hhbGxlbmdlV2luZG93U2l6ZSI6IjA1In0&threeDSSessionData=b3JkZXItMTAwNDI%3D
  1. Take the result from the RReq and answer it at once. The RReq carries the values the authorisation needs. eci is scheme-specific; the value below is illustrative.
JSON
{
  "messageType": "RReq",
  "messageVersion": "2.2.0",
  "messageCategory": "01",
  "threeDSServerTransID": "8a880dc0-d2d2-4067-bcb1-b08d1690b26e",
  "acsTransID": "d7c1ee99-9478-44a6-b1f2-391e29c6b340",
  "dsTransID": "f25084f0-5b16-4c0a-ae5d-b24808a95e4b",
  "transStatus": "Y",
  "authenticationType": "02",
  "authenticationValue": "AAABBEg0VhI0VniQEjRWAAAAAAA=",
  "eci": "05",
  "interactionCounter": "01"
}

Store the result, then return the RRes before any other work. Value 01 of resultsStatus means the results request was received for further processing.

JSON
{
  "messageType": "RRes",
  "messageVersion": "2.2.0",
  "threeDSServerTransID": "8a880dc0-d2d2-4067-bcb1-b08d1690b26e",
  "acsTransID": "d7c1ee99-9478-44a6-b1f2-391e29c6b340",
  "dsTransID": "f25084f0-5b16-4c0a-ae5d-b24808a95e4b",
  "resultsStatus": "01"
}
  1. Accept the final CRes as a signal only. The ACS posts cres and threeDSSessionData to the notificationURL. The decoded CRes has challengeCompletionInd Y and a transStatus that must equal the RReq status. The checkout then closes the iframe and authorises with the stored RReq values.

  2. Repeat in the app channel. With deviceChannel 01, the AReq carries sdkAppID, sdkEncData, sdkEphemPubKey, sdkReferenceNumber, and sdkTransID, and the ARes returns acsSignedContent instead of acsURL. The 3DS SDK then exchanges encrypted CReq and CRes messages with the ACS directly, so the requestor app does not build or read the challenge messages. Test on real devices for app backgrounding, a network change during the challenge, and the payer closing the challenge screen.

  3. Check every run automatically. Export the message log per transaction as JSON lines and run the checker below. It reports unreadable lines instead of stopping.

Python
import json, sys
from datetime import datetime

FIRST_CREQ_S = 30   # ACS window from ARes C to the first CReq (Core Specification 5.5.1)
RRES_BUDGET_S = 3   # DS window for the RRes in v2.0.0 Req. 5.60; confirm with each DS

def ts(event):
    return datetime.fromisoformat(event["ts"])

def check(path):
    events, errors = [], []
    with open(path, encoding="utf-8") as f:
        for n, line in enumerate(f, 1):
            try:
                events.append(json.loads(line))
            except json.JSONDecodeError:
                errors.append(f"line {n}: not JSON, skipped")
    by_type = {}
    for e in events:
        by_type.setdefault(e.get("type"), []).append(e)
    ares = next((e for e in by_type.get("ARes", []) if e.get("transStatus") == "C"), None)
    creq = (by_type.get("CReq") or [None])[0]
    rreq = (by_type.get("RReq") or [None])[0]
    rres = (by_type.get("RRes") or [None])[0]
    auth = (by_type.get("Authorisation") or [None])[0]
    cres = next((e for e in by_type.get("CRes", []) if e.get("challengeCompletionInd") == "Y"), None)
    if ares and creq and (ts(creq) - ts(ares)).total_seconds() > FIRST_CREQ_S:
        errors.append("first CReq sent after the 30-second window")
    if rreq and rres and (ts(rres) - ts(rreq)).total_seconds() > RRES_BUDGET_S:
        errors.append("RRes slower than the DS budget")
    if auth and not rreq:
        errors.append("authorisation without an RReq")
    if auth and rreq:
        if ts(auth) < ts(rreq):
            errors.append("authorisation sent before the RReq")
        for field in ("authenticationValue", "eci"):
            if auth.get(field) != rreq.get(field):
                errors.append(f"authorisation {field} differs from the RReq")
    if cres and rreq and cres.get("transStatus") != rreq.get("transStatus"):
        errors.append("final CRes status differs from the RReq status")
    return errors

if __name__ == "__main__":
    problems = check(sys.argv[1])
    print("PASS" if not problems else "FAIL\n" + "\n".join(problems))
    sys.exit(1 if problems else 0)

The pitfall: two clocks the vendor sandbox does not show

The first clock starts at the ARes. Under section 5.5.1 of the Core Specification (version 2.0.0, Req. 5.36 and 5.42), the ACS waits 30 seconds for the first CReq after it sends an ARes with status C, then waits 10 minutes after each challenge screen; the 3dsecure.io challenge flow guide states the same two limits. When the first window expires, the ACS sends an RReq with status N. In version 2.2.0 the challengeCancel value 05 means that the first CReq was not received by the ACS. A checkout that runs a slow fraud check or a second page load between the ARes and the iframe post will fail this way only under production latency. Test it by delaying the CReq post to 25 seconds and to 35 seconds with a manual challenge card, and assert that the second case shows a retry path and never authorises.

The second clock starts at the RReq. The ACS formats the final CRes only after it receives the RRes, so the payer's browser waits on the merchant's own handler. Version 2.0.0 (Req. 5.60) gave the 3DS Server 3 seconds from the RReq to answer, and the DS ends processing when the time runs out (Req. 5.61). Confirm the current value in the specification version you run and with each DS. The sandbox DS answers in milliseconds and has no load, so the defect appears only when the RReq handler runs the authorisation or waits on a database lock. Inject delays of 2.5 and 3.5 seconds into the handler and assert that the checkout still completes: with the stored RReq result when the CRes never arrives, and without a second authentication.

Test matrix

CaseTriggerExpected resultEvidence
Challenge pass, browserLast four 2070RReq Y, authorisation with RReq valuesMessage log, authorisation request
Challenge fail, browserLast four 2071RReq N, no authorisationMessage log, checkout screen
Manual challenge, cancelledLast four 2072, payer cancelsRReq with challengeCancel 01, no chargeMessage log, order state
First CReq lateCReq posted at 35 secondsRReq N, challengeCancel 05, retry offeredTiming log
Slow RRes3.5-second handler delayOne authentication, one authorisationTiming log, authorisation count
3DS Method timeoutSecond digit 2AReq with threeDSCompInd NBrowser network log
CRes and RReq mismatchTampered cres fieldCheckout uses the RReq resultChecker output
App backgroundedHome button during challengeChallenge resumes or ends with a clear resultDevice log

We run this matrix inside payment flow testing for payment acquiring platforms, card issuers, and Banking-as-a-Service card programmes, and hand over each case with its message log and timing.

Scope your testing

Send FintechAssuranceLabs your product, segment, money flows, and the standards that need evidence, and get a proposed test scope with its deliverables.

Scope your testing