Miracle Docs

3D Secure

3D Secure (3DS) is a security protocol that adds an authentication step during online card payments. Miracle decides whether to initiate 3DS based on risk signals, your merchant configuration, and regulatory requirements. If 3DS is initiated, the customer may be asked to verify their identity — typically through a one-time code, biometric, or banking app approval. This reduces fraud and shifts liability for chargebacks from you to the issuing bank.

Miracle handles 3DS automatically for HPP integrations. For direct API integrations, you need to handle the requires_action status and redirect the customer.


How 3DS works

  1. You submit a payment (via HPP or API).
  2. Miracle evaluates whether 3DS is needed based on risk signals, your merchant configuration, and regulatory requirements (e.g. PSD2/SCA).
  3. If 3DS is required, Miracle passes the 3DS decision and transaction data to the payment provider, which handles the 3DS protocol via its MPI (Merchant Plug-In).
  4. The provider initiates 3DS with the card network and issuer. The issuer then decides how to authenticate the cardholder — either frictionless (no customer interaction) or via a challenge (redirect, in-app prompt, or biometric).
  5. If challenged, the customer completes authentication.
  6. The payment continues processing automatically.
  7. The result includes a threeDSResult object with the authentication outcome.

Not every payment triggers a 3DS challenge. After 3DS is initiated, the issuer may approve the transaction without one (a "frictionless" flow) based on its own risk analysis, transaction history, or exemptions you requested.


HPP integration (automatic)

If you use the Hosted Payment Page, 3DS is handled entirely within the checkout experience. There is nothing additional to build.

  • The customer authenticates inline on the checkout page.
  • After authentication, the payment completes automatically.
  • You receive the result via webhook as normal (payment.succeeded or payment.failed).
  • The threeDSResult field is included in the payment object.

Recommended for most merchants. HPP handles all 3DS complexity for you — redirects, challenge rendering, timeouts, and fallback flows.


Direct API integration (requires handling)

If you use the direct API (POST /v1/payments), you need to handle the case where the payment requires customer authentication before it can proceed.

Flow

  1. Create a payment with confirm: true and a returnUrl.
  2. If 3DS is required, the payment returns with status: "requires_action" and an action object.
  3. Redirect the customer to the URL in action.url.
  4. The customer completes authentication on their bank's page.
  5. The bank redirects the customer back to your returnUrl with result parameters.
  6. Call POST /v1/payments/{id}/complete-action with the redirect parameters.
  7. The payment continues processing. You receive the final result via webhook.

Creating the payment

Include returnUrl so Miracle knows where to send the customer after authentication:

curl -X POST https://api.miracle.com/v1/payments \
  -H "Authorization: Bearer sk_test_your_secret_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1234" \
  -d '{
    "amount": {
      "currency": "EUR",
      "valueMinor": 5000
    },
    "merchantReference": "order-1234",
    "paymentMethod": {
      "type": "card",
      "token": "tok_abc123"
    },
    "confirm": true,
    "returnUrl": "https://your-site.com/payment/return"
  }'
const response = await fetch('https://api.miracle.com/v1/payments', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_test_your_secret_key',
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order-1234',
  },
  body: JSON.stringify({
    amount: { currency: 'EUR', valueMinor: 5000 },
    merchantReference: 'order-1234',
    paymentMethod: {
      type: 'card',
      token: 'tok_abc123',
    },
    confirm: true,
    returnUrl: 'https://your-site.com/payment/return',
  }),
});

const { data: payment } = await response.json();

if (payment.status === 'requires_action') {
  // Redirect customer to payment.action.url
}

The requires_action response

When Miracle determines that 3DS authentication is required, the payment response looks like this:

{
  "data": {
    "id": "pay_abc123",
    "status": "requires_action",
    "amount": {
      "currency": "EUR",
      "valueMinor": 5000
    },
    "merchantReference": "order-1234",
    "action": {
      "type": "redirect",
      "url": "https://acs.bank.com/3ds/authenticate?id=xyz"
    },
    "captureMethod": "automatic",
    "financialStatus": "none",
    "createdAt": "2026-04-09T10:00:00Z",
    "updatedAt": "2026-04-09T10:00:01Z"
  }
}

Action types

The action.type field tells you how to handle the authentication step:

TypeDescriptionHow to handle
redirectStandard 3DS2 redirect.Redirect the customer's browser to action.url.
html_form3DS1 form-based redirect (legacy). The provider returns an HTML form with hidden fields (e.g. PaReq, MD) that must be POSTed to the ACS.Render an auto-submitting form using action.payload.url, action.payload.method, and action.payload.fields.

For redirect actions, redirect the customer's browser to action.url. After they complete authentication, the bank sends them back to your returnUrl.

For html_form actions, build a form that auto-submits to the ACS URL:

<form method="POST" action="{action.payload.url}">
  <!-- Render each entry in action.payload.fields as a hidden input -->
  <input type="hidden" name="PaReq" value="{action.payload.fields.PaReq}" />
  <input type="hidden" name="MD" value="{action.payload.fields.MD}" />
  <input type="hidden" name="TermUrl" value="https://your-site.com/payment/return" />
</form>
<script>document.forms[0].submit();</script>

HPP handles both action types automatically. You only need to handle action types if you use the direct API integration.

Do not poll for status. Use the complete-action endpoint after the redirect, and rely on webhooks for the final result. Polling is unreliable and may hit rate limits.


Completing the action

When the customer returns to your returnUrl, the URL includes query parameters with the authentication result. Pass these to the complete-action endpoint:

curl -X POST https://api.miracle.com/v1/payments/pay_abc123/complete-action \
  -H "Authorization: Bearer sk_test_your_secret_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: complete-order-1234" \
  -d '{
    "redirectResult": "eyJhbGciOiJSUzI1NiJ9..."
  }'
app.get('/payment/return', async (req, res) => {
  const { redirectResult, cres, MD, PaRes } = req.query;

  const response = await fetch(
    `https://api.miracle.com/v1/payments/${paymentId}/complete-action`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.MIRACLE_SECRET_KEY}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': `complete-${paymentId}`,
      },
      body: JSON.stringify({
        redirectResult,
        // 3DS1 fallback fields (if present)
        md: MD,
        paRes: PaRes,
        // 3DS2 challenge response (if present)
        cres,
      }),
    }
  );

  const { data: payment } = await response.json();

  if (payment.status === 'succeeded' || payment.status === 'captured') {
    res.redirect('/payment/success');
  } else if (payment.status === 'requires_capture') {
    res.redirect('/payment/success'); // Capture later
  } else {
    res.redirect('/payment/failed');
  }
});

The complete-action request body accepts these fields:

FieldTypeDescription
redirectResultstringResult string from the returnUrl query parameters.
mdstring3DS1: Merchant Data from ACS redirect.
paResstring3DS1: Payer Authentication Response.
cresstring3DS2: Challenge Response.

Include whichever fields are present in the redirect. Not all fields will be populated — it depends on whether 3DS1 or 3DS2 was used.

Idempotency: The complete-action endpoint is idempotent. If you call it again with the same redirectResult, you get the same response. Always set the Idempotency-Key header.


3DS result in webhooks

After 3DS authentication, the payment.succeeded (or payment.failed) webhook event includes a threeDSResult object on the payment:

{
  "id": "evt_xyz789",
  "type": "payment.succeeded",
  "data": {
    "id": "pay_abc123",
    "status": "succeeded",
    "amount": {
      "currency": "EUR",
      "valueMinor": 5000
    },
    "threeDSResult": {
      "version": "2.2",
      "status": "authenticated",
      "eci": "05",
      "cavv": "AAABBEg0VhI0VniQEjRWAAAAAAA=",
      "dsTransId": "f25084f0-5b16-4c0a-ae5d-b24808571734",
      "enrolled": "Y",
      "liabilityShift": true,
      "challengeFlow": "challenge",
      "challenged": true
    },
    "merchantReference": "order-1234",
    "createdAt": "2026-04-09T10:00:00Z",
    "updatedAt": "2026-04-09T10:00:15Z"
  },
  "occurredAt": "2026-04-09T10:00:15Z"
}

threeDSResult fields

FieldTypeDescription
versionstring3DS protocol version ("1.0", "2.1", "2.2").
statusstringAuthentication outcome: authenticated, attempted, not_authenticated, unavailable, rejected, or not_performed.
ecistringElectronic Commerce Indicator. Indicates the level of authentication ("05" = fully authenticated, "06" = attempted, "07" = not authenticated). Exact values vary by card network.
cavvstringCardholder Authentication Verification Value. Cryptographic proof of authentication.
dsTransIdstringDirectory Server Transaction ID. Unique identifier for the 3DS transaction.
enrolledstringWhether the card is enrolled in 3DS: "Y" (yes), "N" (no), "U" (unknown).
liabilityShiftbooleantrue if chargeback liability shifted to the issuer. This is the most important field for fraud protection.
challengeFlowstringHow the authentication was performed: "frictionless" (no customer interaction), "challenge" (customer verified), "decoupled", or "none".
challengedbooleanWhether the customer was presented with a challenge.

Liability shift is the key benefit of 3DS. When liabilityShift is true, the issuing bank bears the cost of fraudulent chargebacks — not you. This applies even when the customer passes authentication without a challenge (frictionless flow).


3DS preference (optional)

For direct API integrations, you can optionally include a threeDSPreference object when creating a payment to control 3DS behavior:

{
  "amount": { "currency": "EUR", "valueMinor": 5000 },
  "merchantReference": "order-1234",
  "paymentMethod": { "type": "card", "token": "tok_abc123" },
  "confirm": true,
  "returnUrl": "https://your-site.com/payment/return",
  "threeDSPreference": {
    "mode": "auto"
  }
}
FieldValuesDescription
mode"auto", "force", "skip"auto (default): Miracle decides based on risk and regulation. force: always request 3DS. skip: skip 3DS if allowed by regulations.
challengePreference"no_preference", "no_challenge", "challenge_requested", "challenge_mandate"Hint to the issuer about whether to present a challenge. The issuer makes the final decision.
exemption"low_value", "transaction_risk_analysis", "trusted_beneficiary", "secure_corporate", "recurring", "delegated", "out_of_scope"Request a specific SCA exemption. The issuer may or may not grant it.

In the EU (PSD2/SCA), 3DS is required for most card payments. Setting mode: "skip" does not bypass regulatory requirements — the payment may still be declined by the issuer if SCA is mandated.


Merchant-level 3DS defaults

Your merchant account has default 3DS settings that apply when you don't include a threeDSPreference in the payment request. These defaults can be configured by your account manager or through the portal:

SettingDefaultDescription
defaultMode"auto"The 3DS mode used when the request has no threeDSPreference.mode. Same values as mode above.
autoUpgradeOnSoftDeclinetrueIf a payment is soft-declined because 3DS was not performed, Miracle automatically retries with 3DS enabled.
attemptNon3dstrueIf 3DS is unavailable (e.g. card not enrolled), proceed with the payment without 3DS. When false, the payment is declined instead.
requireScafalseWhen true, Miracle always requires 3DS unless a valid exemption is provided. Useful for merchants who want to enforce SCA beyond regulatory requirements.

autoUpgradeOnSoftDecline is not currently active. Use mode: "force" to guarantee 3DS on all transactions.

Per-request overrides take priority. If you include threeDSPreference in the payment request, it overrides the merchant-level defaults for that payment.


Testing 3DS

In sandbox mode, specific test cards trigger different 3DS scenarios. See Test Cards for the full list, including cards that:

  • Trigger a 3DS challenge (customer authentication required)
  • Complete with frictionless 3DS (no challenge, liability shift granted)
  • Fail 3DS authentication
  • Simulate cards not enrolled in 3DS

On this page