Most 3DS support tickets that reach an escalation queue contain an error and no explanation. Someone has a code, or an HTTP status, or neither, and nobody can say whether the problem is theirs or the counterparty’s.
3DS error codes carry more information than most teams extract, but only if you understand two things first: that protocol errors and transport errors are different layers with different rules, and that the specification deliberately leaves parts of the handling to you.
Two layers, and people conflate them constantly
The Core Specification covers error cases on the 3DS protocol itself, meaning message validation, conflict and timeout, and the use of the error message.
Underneath that sits HTTP, and the specification adds no additional requirements for the message exchanges between components. The expectation is that components comply with the HTTP standard and use a 200 response status code for normal 3DS message exchanges.
So HTTP status codes are used to report errors outside 3DS message processing. EMVCo gives concrete examples. The post method is mandatory for 3DS messages, so using PUT may produce a 405 Method Not Allowed from the HTTP layer. A server unable to respond due to traffic volume may return 429 too many requests. An invalid content type may produce 415 unsupported media type. And to protect against denial of service, cross-site scripting or injection attacks, a server may return 400 bad request or 403 forbidden.
Note what that means diagnostically. A 4xx status is often not a 3DS problem at all. It is your request never reaching 3DS message processing. Teams that treat every failure as a protocol error spend a long time reading the wrong specification.
One currency note for implementers: EMVCo points out that the HTTP standard it originally referenced is obsolete though still widely cited, and has been replaced by a later set of documents, with additional status codes in a separate one. Work from the current standards.
The identification problem
A great deal of error handling in the specification is conditioned on whether a specific transaction can be identified. That raises an obvious question: identified how?
The answer is that it depends on the error and on the implementation. In some situations one or more of the identification elements, meaning the various transaction identifiers, may be recoverable and can be used. Additionally, an implementation-specific session identifier created when the connection between two components is established can be used, and that works even where the transaction identifiers in the messages are not recoverable.
The Core Specification does not mandate a method. It is up to each implementation.
This is worth knowing because it explains a real difference between platforms. Two systems receiving the same malformed message can differ in whether they can attribute it to a transaction at all, and therefore in whether you get a diagnosable incident or an unexplained gap. If your platform relies solely on identifiers inside the message, anything that corrupts those identifiers becomes untraceable.
The same reasoning applies to recognising messages. Where a message type is not readable, identification elements or the presence of elements specific to a message type may allow recognition, and again no method is mandated. Where a component does not recognise a message at all, it may return an HTTP 400 or equivalent instead of an error message.
Codes worth knowing about
Two changes in version 2.3.1.1 are worth flagging because they affect how you read your own logs.
Error codes 312 and 313 were introduced to replace a single earlier code, so that two distinct situations could be told apart. One covers a Directory Server or 3DS Server receiving multiple results messages during a transaction. The other covers receiving a results message where the transaction status in the corresponding authentication response was not one of the values that should lead to one.
That split matters. Under the older single code, both looked the same, and they have different causes and different fixes. For components still on version 2.1.0 or 2.2.0 it remains acceptable to return the older code as in their existing implementation, so a mixed estate will report the same underlying problem two different ways.
There are also two codes that distinguish a missing required element from an invalid one. Where a conditionally optional or optional field is sent as empty, the receiving component returns the invalid-format code. Where the empty field is a JSON object containing mandatory data, the receiving component may return the missing-required-element code instead.
The practical consequence is that sending an empty string is not the same as omitting a field, and it produces a different error. Empty is a value; absent is not.
What a component should do with a broken error message
An underappreciated corner. The Core Specification does not define how a component should behave when it receives an error message that is itself in error.
EMVCo’s guidance is that where a specific transaction can be identified from the broken error message, the component may format and send a new, correctly formed error message to the relevant components to complete the transaction. The example given is a Directory Server receiving a malformed error message from an ACS and sending a correctly formatted one on to the 3DS Server to close the transaction.
And one rule that prevents a loop: a receiving component never responds to the sender of an error message, including when that error message is in error.
Being tolerant where tolerance is correct
There is a pattern in EMVCo’s guidance worth internalising, because it runs against the instinct to validate strictly.
On message encoding, where the encoding of received content is not strictly valid but the receiving component is able to recover and decrypt it, the guidance in several cases is to proceed rather than return an error. EMVCo also strongly recommends that ACSs relax one specific encoding check on incoming challenge requests in browser flows, because certain merchant implementations do not meet the requirement and rejecting them serves nobody.
The principle is that rejecting a recoverable message costs a transaction and gains nothing. Strictness is a virtue when producing messages and frequently a liability when consuming them.
Frequently Asked Questions
Is an HTTP 400 a 3DS error?
Usually not in the protocol sense. Components should use 200 for normal 3DS message exchanges, and other status codes report problems outside 3DS message processing, including malformed requests, wrong methods, wrong content types and protective responses to suspected attacks. It usually means the request never reached 3DS message processing.
How does a component identify a transaction when the message is broken?
It depends on the error and the implementation. Transaction identifiers may be recoverable, or an implementation-specific session identifier created when the connection was established can be used, which works even when the identifiers in the message are not recoverable. No method is mandated.
Why do we see two different codes for what looks like the same problem?
Version 2.3.1.1 split an earlier single code into two, so that multiple results messages and an unexpected results message could be distinguished. Components still on version 2.1.0 or 2.2.0 may continue returning the older code, so a mixed estate reports the same underlying issue two ways.
Should we send an empty string rather than omit an optional field?
No. An empty conditionally optional or optional field returns an invalid-format error, and an empty JSON object containing mandatory data may return a missing-element error. Omitting a field you do not have is correct; sending it empty is an error.
Diagnosing 3DS failures in production? TestLabs is the GPayments 3DS testing environment. Speak with a 3DS specialist about error handling and diagnostics.
