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 noauthenticationValueand noeci: 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.
| Step | Message | Path | Fields the test asserts |
|---|---|---|---|
| 1 | 3DS Method | Browser to ACS, hidden iframe | threeDSMethodData with threeDSServerTransID and threeDSMethodNotificationURL |
| 2 | AReq | 3DS Server to Directory Server (DS) to ACS | deviceChannel, messageCategory, threeDSCompInd, threeDSRequestorChallengeInd, notificationURL |
| 3 | ARes | ACS to DS to 3DS Server | transStatus, acsURL or acsSignedContent, acsChallengeMandated, acsTransID |
| 4 | CReq and CRes | Browser or 3DS SDK to ACS, repeated per challenge screen | challengeWindowSize, challengeCompletionInd |
| 5 | RReq | ACS to DS to 3DS Server | transStatus, authenticationValue, eci, interactionCounter, challengeCancel |
| 6 | RRes | 3DS Server to DS to ACS | resultsStatus |
| 7 | Final CRes | ACS through the browser to notificationURL | transStatus 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
-
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
7xstarts a challenge that passes (70), fails (71), or waits for manual input (72). Its published card 7000100911112070 runs a 2.2.0 challenge that returnsYin the RReq. Card numbers for authorisation and decline testing belong to the separate test card numbers guide. -
Run the 3DS Method and record its completion. Post
threeDSMethodDatato thethreeDSMethodURLfrom the card range data, then setthreeDSCompIndtoYwhen the notification arrives,Nwhen it does not, andUwhen 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. -
Send a browser AReq that requests a challenge. Value
04ofthreeDSRequestorChallengeIndrequests a challenge as a mandate. The request contains the card number inacctNumber, which is cardholder data: log it masked, even in a sandbox. Fields not relevant to the challenge are omitted.
{
"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.
- Start the challenge from the ARes. A browser ARes with status
Creturns theacsURL.
{
"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.
POST /challenge HTTP/1.1
Host: acs.example.com
Content-Type: application/x-www-form-urlencoded
creq=eyJ0aHJlZURTU2VydmVyVHJhbnNJRCI6IjhhODgwZGMwLWQyZDItNDA2Ny1iY2IxLWIwOGQxNjkwYjI2ZSIsImFjc1RyYW5zSUQiOiJkN2MxZWU5OS05NDc4LTQ0YTYtYjFmMi0zOTFlMjljNmIzNDAiLCJtZXNzYWdlVHlwZSI6IkNSZXEiLCJtZXNzYWdlVmVyc2lvbiI6IjIuMi4wIiwiY2hhbGxlbmdlV2luZG93U2l6ZSI6IjA1In0&threeDSSessionData=b3JkZXItMTAwNDI%3D- Take the result from the RReq and answer it at once. The RReq carries the values the authorisation needs.
eciis scheme-specific; the value below is illustrative.
{
"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.
{
"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"
}-
Accept the final CRes as a signal only. The ACS posts
cresandthreeDSSessionDatato thenotificationURL. The decoded CRes haschallengeCompletionIndYand atransStatusthat must equal the RReq status. The checkout then closes the iframe and authorises with the stored RReq values. -
Repeat in the app channel. With
deviceChannel01, the AReq carriessdkAppID,sdkEncData,sdkEphemPubKey,sdkReferenceNumber, andsdkTransID, and the ARes returnsacsSignedContentinstead ofacsURL. 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. -
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.
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
| Case | Trigger | Expected result | Evidence |
|---|---|---|---|
| Challenge pass, browser | Last four 2070 | RReq Y, authorisation with RReq values | Message log, authorisation request |
| Challenge fail, browser | Last four 2071 | RReq N, no authorisation | Message log, checkout screen |
| Manual challenge, cancelled | Last four 2072, payer cancels | RReq with challengeCancel 01, no charge | Message log, order state |
| First CReq late | CReq posted at 35 seconds | RReq N, challengeCancel 05, retry offered | Timing log |
| Slow RRes | 3.5-second handler delay | One authentication, one authorisation | Timing log, authorisation count |
| 3DS Method timeout | Second digit 2 | AReq with threeDSCompInd N | Browser network log |
| CRes and RReq mismatch | Tampered cres field | Checkout uses the RReq result | Checker output |
| App backgrounded | Home button during challenge | Challenge resumes or ends with a clear result | Device 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.
Related
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