Post Response Module

Some payments cannot be completed in a single server-to-server call. The issuer may need to challenge your customer, or the payment method may need to send them to an external provider. When that happens, the BR-DGE REST API returns a post-response action: a small instruction telling you that something must happen in the customer's browser before the payment can proceed.

The Post Response Module carries out that instruction for you. You hand it the action object exactly as your server received it, and the module renders the challenge, talks to the relevant provider, and resolves with the result you need to finish the payment.

📘

Why this matters

Every PSP implements 3-D Secure differently: different iframes, different tokens, different callbacks. This module absorbs those differences so your checkout code only ever deals with one flow. If you switch or add a PSP through Smart Routing, your front-end code should not need to change.


How it works

A post-response action is always a three-way conversation between your server, your customer's browser, and BR-DGE. Your server never handles the challenge itself — it only passes the action along.

sequenceDiagram
    autonumber
    participant Browser as Customer's browser
    participant Server as Your server
    participant BRDGE as BR-DGE REST API
    participant PSP as PSP / Issuer
 
    Browser->>Server: Submit checkout details
    Server->>BRDGE: POST /v1/payments
    BRDGE-->>Server: 202 Accepted, actionRequired: true, action: { ... }
    Server-->>Browser: The action object
    Browser->>Browser: handleAction(action)
    Browser->>PSP: SDK performs the challenge
    PSP-->>Browser: Authentication outcome
    Browser-->>Server: Resolved result (code, nonce)
    Server->>BRDGE: POST /v1/payments with the nonce
    BRDGE-->>Server: 201 Approved, or another action
    Server-->>Browser: Final outcome

In words:

  1. Your server creates a payment and receives a 202 Accepted response containing actionRequired: true and an action object. For 3-D Secure this arrives with response code 2001.
  2. Your server passes the action object to your client. Nothing else from the response is needed.
  3. Your client passes it to handleAction. The module resolves with a result — typically a nonce under response code 2002.
  4. Your client sends that result to your server, which completes the payment using a ProviderThreeDSecureNonce payment instrument.
  5. Depending on the PSP, step 4 may return another action. If it does, repeat from step 2.
❗️

Step 5 is not optional

A single payment can require more than one round of authentication. Build your integration as a loop that keeps calling handleAction while actionRequired is true, rather than assuming one challenge is enough. Integrations that handle only the first action will fail intermittently on certain PSPs and card ranges.

For the wider context, see the 3-D Secure Payment Flow and Post-Response Actions.


Before you begin

You will need:

RequirementNotes
A Client API KeySafe to expose in the browser. Never use a Server API Key in client code.
The Comcarde JavaScript ClientThis module is a plugin and cannot be used on its own.
A server that can relay the action object to your clientAny transport is fine: JSON response, template variable, WebSocket.

The Web SDK is plain ES5 JavaScript. There is no build step, no bundler and no framework requirement — the scripts attach a global comcarde object to the page.

🚧

Promise support

handleAction returns a Promise. If you support browsers without native Promise support, load a polyfill before the SDK scripts. If you are unsure whether your SDK version bundles one, ask support.


Install

Add the module script to your page, after the client script. Use the sandbox asset host while developing and the production host for live traffic.

<script src="https://sandbox-assets.comcarde.com/web/v2/js/client.min.js"></script>
<script src="https://sandbox-assets.comcarde.com/web/v2/js/post-response-action.min.js"></script>
<script src="https://assets.comcarde.com/web/v2/js/client.min.js"></script>
<script src="https://assets.comcarde.com/web/v2/js/post-response-action.min.js"></script>
📘

Dedicated environments

If you have a dedicated BR-DGE environment, your asset host will differ from the two above. Contact support if you are unsure which to use.

Load order matters: client.min.js must be parsed before the module script runs, and both must be present before you call comcarde.client.create.


Quick start

The minimum viable integration. Create the client, create the module, hand it an action.

comcarde.client.create(
  {
    authorization: clientApiKey // your Client API Key
  },
  function (clientErr, clientInstance) {
    if (clientErr) {
      console.error('Client creation failed', clientErr);
      return;
    }
 
    comcarde.postResponseAction.create(
      {
        client: clientInstance
      },
      function (err, postResponseActionInstance) {
        if (err) {
          console.error('Module creation failed', err);
          return;
        }
 
        // The 'action' object from your server's payment response.
        var action = actionFromServerResponse;
 
        postResponseActionInstance
          .handleAction(action)
          .then(function (response) {
            // The action completed. Inspect response.code to decide what to do next.
            console.log(response.code, response.message);
          })
          .catch(function (handleErr) {
            console.error('handleAction failed', JSON.stringify(handleErr));
          });
      }
    );
  }
);
👍

Parse once, not twice

handleAction expects a JavaScript object, not a JSON string. If your server sends the whole payment response as JSON and your client already parses it, pass response.action straight through. Only call JSON.parse if you are handling a raw string, for example a value injected into a template:

var action = JSON.parse(actionJsonFromServerResponse); // string in
var action = paymentResponse.action;                   // already an object

Calling JSON.parse on an object is a common cause of THREE_D_SECURE_ERROR_INVALID_ACTION_TYPE.


API reference

comcarde.postResponseAction.create(options, callback)

Creates a module instance bound to a client instance.

ParameterTypeRequiredDescription
options.clientObjectYesA client instance from comcarde.client.create.
options.stylesObjectNoPresentation overrides for the challenge window. See Controlling the challenge z-index.
callbackFunctionYesNode-style callback, function (err, instance).

The callback receives an error or an instance — always check err before using the instance. Errors follow the standard Web SDK error shape:

{
  "code": "4001",
  "message": "Access denied."
}

Create the instance once when your checkout page loads and reuse it. You do not need a fresh instance per action.

instance.handleAction(action[, callbacks])

Executes a post-response action. Returns a Promise.

ParameterTypeRequiredDescription
actionObjectYesThe action object from the REST API response, unmodified.
callbacksObjectNoProgress callbacks. See Callbacks.
OutcomeMeaning
ResolvesThe action ran to completion. This does not mean the payment succeeded — check response.code.
RejectsThe action could not be completed. See Error reference.

The action object

Pass this through verbatim. Do not reshape it, add fields, or strip fields — the module uses type to select the correct PSP-specific handler.

{
  "action": {
    "type": "3D_SECURE",
    "paymentId": "22669f90-5bf0-45df-ba8c-ea6d4235a5da",
    "data": {
      "clientToken": "aaabbb",
      "verifyCardPayload": "payload"
    }
  }
}
FieldTypeDescription
typeStringThe kind of action, for example 3D_SECURE. Determines how the module handles it.
paymentIdStringThe BR-DGE payment this action belongs to. Useful for correlating logs.
dataObjectOpaque, PSP-specific payload. Contents vary; treat as a black box.
🚧

Do not branch on data

The keys inside data differ between PSPs and may change as connections are added. Reading them in your own code couples your checkout to a specific PSP and defeats the purpose of the module.

The resolved response

FieldTypeDescription
codeStringA BR-DGE response code describing the outcome. Branch on this.
messageStringHuman-readable explanation. Useful for logs; not intended for display to customers.
paymentIdStringThe payment this result belongs to.
nonceStringPresent on 2002. Use this on your server to complete the payment.
threeDSecureInfoObjectOptional authentication detail. See below.

The codes you are most likely to see:

CodeMeaningWhat to do
20023-D Secure additional payment with nonce required.Send nonce to your server and complete the payment.
20043-D Secure processing error.Treat as a failed authentication attempt. Decide whether to retry or fail the checkout.

Other codes are possible depending on the payment method and PSP, so write your handler with a default branch rather than assuming only the two above.

threeDSecureInfo

Supplied when the PSP returns authentication detail. Always check the object exists before reading it.

FieldTypeDescription
pspStatusStringStatus description as returned by the PSP. Raw, PSP-specific text — log it, don't parse it.
threeDSecureVersionStringThe protocol version used, for example "2".
liabilityShiftedBooleanWhether liability for fraud has moved to the issuer.
liabilityShiftPossibleBooleanWhether the payment instrument was eligible for 3-D Secure at all.

Callbacks

You can optionally pass functions that fire at points during execution, which is useful for managing spinners and re-enabling your pay button.

var callbacks = {
  windowClosed: function () {
    // The customer dismissed the challenge window.
    hideSpinner();
    enablePayButton();
  },
  actionCompleted: function () {
    // The action finished; the promise is about to resolve.
    hideSpinner();
  }
};
 
postResponseActionInstance.handleAction(action, callbacks);
CallbackFires when
windowClosedThe challenge window or overlay is closed, including when the customer dismisses it.
actionCompletedThe action has completed.
❗️

Callback support is not universal

Not every action type supports callbacks, and the set available depends on the action type and the PSP handling your payment. Treat them as progress hints, not guarantees — never rely on actionCompleted as your only completion signal. Put the logic that must always run in .then() and .catch(). Contact support to confirm which callbacks apply to the actions configured on your account.


Controlling the challenge z-index

3-D Secure challenges are rendered in a modal window layered over your checkout page. If your own page has high-stacking elements — a sticky header, a cookie banner, a basket drawer, a loading overlay — they can appear on top of the challenge, or hide it entirely. The customer sees a blank or partially obscured overlay and abandons.

Pass threeDSModalZIndex in the styles object when creating the module to place the challenge window at a specific z-index:

comcarde.postResponseAction.create(
  {
    client: clientInstance,
    styles: {
      threeDSModalZIndex: 12000
    }
  },
  function (err, postResponseActionInstance) {
    if (err) {
      console.error('Module creation failed', err);
      return;
    }
 
    // ... handleAction as usual
  }
);
StyleTypeDescription
threeDSModalZIndexNumberForces the 3-D Secure challenge window to render at the specified z-index.

Set this to a value above the highest z-index on your checkout page. Audit what you actually use rather than guessing — third-party scripts such as chat widgets, consent managers, and session-recording tools frequently sit in the tens of thousands.

👍

Configure it once

styles is set when the module is created, not per action, so a single value covers every challenge on the page. Set it during your normal checkout initialisation rather than trying to adjust it when an action arrives.

🚧

z-index only competes within the same stacking context

If an ancestor of the challenge creates its own stacking context — through transform, filter, opacity below 1, will-change, or its own position plus z-index — raising threeDSModalZIndex will not lift the challenge above elements outside that context. If a large value makes no difference, the problem is the stacking context, not the number. Inspect the ancestor chain before increasing it further.

Because this affects presentation only, it cannot cause an authentication failure — but it is a common cause of apparent ones, where the challenge renders correctly and the customer simply cannot see or interact with it. If you see an unexplained cluster of THREE_D_SECURE_METHOD_FAILURE errors or windowClosed callbacks, check this before escalating.


Worked example: 3-D Secure

This is the full pattern, including the liability-shift decision and the error path.

comcarde.client.create(
  {
    authorization: clientApiKey // your Client API Key
  },
  function (clientErr, clientInstance) {
    if (clientErr) {
      console.error('Client creation failed', clientErr);
      return;
    }
 
    comcarde.postResponseAction.create(
      {
        client: clientInstance
      },
      function (err, postResponseActionInstance) {
        if (err) {
          console.error('Module creation failed', err);
          return;
        }
 
        var action = paymentResponse.action;
 
        postResponseActionInstance
          .handleAction(action)
          .then(function (response) {
            var paymentId = response.paymentId;
 
            if (response.code === '2002') {
              // Authentication finished. A nonce is available to complete the payment.
              var info = response.threeDSecureInfo;
 
              if (info && info.liabilityShifted) {
                // Authentication succeeded, or the issuer does not support 3-D Secure
                // while the payment method does. Either way, fraud liability now sits
                // with the issuer. Complete the payment on your server.
                completePaymentOnServer(paymentId, response.nonce);
                return;
              }
 
              // Liability has NOT shifted. You may still complete the payment, but you
              // must set threeDSecureRequired to false on the follow-up request, and
              // you carry the fraud risk. This is a commercial decision.
              if (info && info.liabilityShiftPossible) {
                // Instrument was eligible for 3-D Secure but authentication did not
                // succeed. Higher risk.
              } else {
                // Instrument was never eligible for 3-D Secure.
              }
 
              handleUnauthenticatedPayment(paymentId, response.nonce, info);
              return;
            }
 
            if (response.code === '2004') {
              // An error occurred during authentication.
              showRetryableError();
              return;
            }
 
            // Any other code: log it and fail safe rather than silently continuing.
            console.warn('Unexpected code from handleAction', response.code, response.message);
            showGenericError();
          })
          .catch(function (handleErr) {
            // handleErr usually carries a 3-D Secure action error code explaining what
            // went wrong. Include it verbatim in any support request.
            console.error(
              'Exception while processing 3-D Secure action: ' + JSON.stringify(handleErr)
            );
            showGenericError();
          });
      }
    );
  }
);

Deciding what to do when liability has not shifted

liabilityShifted: false is not automatically a decline. It is a risk decision only you can make.

liabilityShiftedliabilityShiftPossibleInterpretationTypical action
trueAuthentication succeeded, or the issuer does not participate while the instrument does. Liability sits with the issuer.Complete the payment.
falsetrueThe instrument was eligible for 3-D Secure, but authentication did not succeed.Highest risk. Most merchants decline, or retry authentication.
falsefalseThe instrument was never eligible for 3-D Secure.Complete only if you accept the fraud risk.

To complete a payment where liability has not shifted, set threeDSecureRequired to false on the follow-up request. Otherwise it will be rejected.

❗️

Regulatory context

Proceeding without a liability shift may conflict with Strong Customer Authentication obligations in your market. Confirm your position before enabling this path in production.


Error reference

When handleAction rejects, the error usually carries one of the codes below. Log the entire error object — not just the code — and include it in any support request along with the paymentId.

General 3-D Secure errors

CodeDescriptionWhere to look first
THREE_D_SECURE_ERRORA problem occurred during the 3-D Secure verification process.Generic. Raise with support, quoting the paymentId.
THREE_D_SECURE_METHOD_FAILURENo nonce was received during the 3-D Secure verification process.The challenge did not complete. Check whether the customer abandoned it, or whether the window was blocked.
THREE_D_SECURE_ACTION_REQUIREDA problem occurred during the 3-D Secure verification process.Generic. Confirm you passed the full action object.
THREE_D_SECURE_ACTION_INVALIDA problem occurred during the 3-D Secure verification process.Generic. Usually a malformed or reshaped action object.
THREE_D_SECURE_UNSUPPORTED_PSPThe PSP for this action is not supported by this version of the SDK.Your SDK version is behind. Update to the latest asset URL and retest.
THREE_D_SECURE_ERROR_NO_RESPONSEThe server did not respond in time. May also indicate a bad request from the issuer's ACS.Transient in many cases. Check for network restrictions or content security policy rules blocking the ACS domain.
THREE_D_SECURE_ERROR_INVALID_ORIGINThis origin is not allowed.Check the origin you submitted on the payment request matches the page actually hosting the challenge.
📘

On the generic descriptions

Three of the codes above share the same description. They are distinct conditions internally, so quote the exact code when contacting support — it materially speeds up diagnosis.

Malformed action errors

These indicate the action object reaching handleAction is not what the module expected. In almost every case the cause is in the relay between your server and your client: partial serialisation, an object being stringified twice, or only part of the payload being forwarded.

CodeDescription
THREE_D_SECURE_ERROR_INVALID_ACTION_TYPEAn invalid action type was provided.
THREE_D_SECURE_ERROR_MISSING_PROPERTY_ACS_URLThe required acsUrl property is missing.
THREE_D_SECURE_ERROR_MISSING_PROPERTY_TOKENThe required token property is missing.
THREE_D_SECURE_ERROR_MISSING_PROPERTY_JWTThe required jwt property is missing.

PSP-specific errors

CodePSPDescription
BARCLAYCARD_THREE_D_SECURE_HTML_ERRORBarclaycard Smartpay FuseThe htmlAnswer was invalid.
STRIPE_THREE_D_SECURE_ERROR_FAILED_TO_LOAD_SDKStripeFailed to load the Stripe SDK.
TRUST_THREE_D_SECURE_ERROR_FAILED_TO_LOAD_SDKTrust PaymentsFailed to load the Trust Payments SDK.
NUVEI_THREE_D_SECURE_METHOD_PAYLOADNuveimethodPayload object missing from threeDSecureAction.
NUVEI_THREE_D_SECURE_C_REQNuveicReq object missing from threeDSecureAction.
ADYEN_THREE_D_SECURE_MISSING_POST_URLAdyenURL missing from the payment response.
ADYEN_THREE_D_SECURE_MISSING_PA_REQAdyenPaReq object missing from threeDSecureAction.
ADYEN_THREE_D_SECURE_MISSING_MDAdyenMD object missing from threeDSecureAction.

The two FAILED_TO_LOAD_SDK errors mean a third-party script could not be fetched from the customer's browser. Check your content security policy allows the PSP's script domains.


Troubleshooting

SymptomLikely cause
comcarde.postResponseAction is undefinedThe module script did not load, or ran before client.min.js. Check load order and the network tab.
Rejects immediately with a missing-property errorThe action object was reshaped, double-parsed, or only partially forwarded from your server. Log it on both sides and compare.
Challenge window never appearsA popup or iframe was blocked. Call handleAction from within the customer's click handler rather than after an intervening await or timeout.
Challenge appears behind your header, overlay or cookie bannerStacking order. Set threeDSModalZIndex above the highest z-index on your page.
Challenge is visible but unclickable, or shows as a blank overlayAnother element is layered on top of it. Same fix as above; if raising the value changes nothing, check for an ancestor creating its own stacking context.
Payment succeeds in sandbox but fails liveMismatched hosts. Confirm the asset host, API subdomain and API key all belong to the same environment.
Works on one card, fails on anotherDifferent card ranges route to different PSPs. Check your integration handles repeat actions and does not read PSP-specific fields from data.
THREE_D_SECURE_METHOD_FAILURE under loadOften customer abandonment rather than a fault. Correlate against your windowClosed callback before escalating — and rule out a z-index problem, which looks identical in your metrics.

Some PSPs require additional fields on the original payment request before 3-D Secure will work at all — in particular browserData, channel and origin. If authentication fails consistently for a given PSP, confirm those are populated before investigating the client.


Testing

Use the sandbox asset host and API subdomain together, and drive challenges with the simulator PSPs.

Worth covering explicitly before you go live:

  • A frictionless authentication that resolves with 2002 and a liability shift.
  • A challenge rendered on your real checkout page, with headers, banners and any third-party widgets present, to confirm the z-index is high enough. A challenge that renders fine in isolation can still be buried in production.
  • A challenge the customer abandons, to verify your windowClosed handling and that your pay button becomes usable again.
  • A payment requiring two consecutive actions, to prove your loop works.
  • A 2004 processing error, to verify your error path.
❗️

PCI compliance in sandbox

Use only mock payment instrument data against the sandbox. Never submit real cardholder data to a test environment.


Related documentation


Did this page help you?