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
- You submit a payment (via HPP or API).
- Miracle evaluates whether 3DS is needed based on risk signals, your merchant configuration, and regulatory requirements (e.g. PSD2/SCA).
- 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).
- 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).
- If challenged, the customer completes authentication.
- The payment continues processing automatically.
- The result includes a
threeDSResultobject 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.succeededorpayment.failed). - The
threeDSResultfield 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
- Create a payment with
confirm: trueand areturnUrl. - If 3DS is required, the payment returns with
status: "requires_action"and anactionobject. - Redirect the customer to the URL in
action.url. - The customer completes authentication on their bank's page.
- The bank redirects the customer back to your
returnUrlwith result parameters. - Call
POST /v1/payments/{id}/complete-actionwith the redirect parameters. - 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:
| Type | Description | How to handle |
|---|---|---|
redirect | Standard 3DS2 redirect. | Redirect the customer's browser to action.url. |
html_form | 3DS1 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:
| Field | Type | Description |
|---|---|---|
redirectResult | string | Result string from the returnUrl query parameters. |
md | string | 3DS1: Merchant Data from ACS redirect. |
paRes | string | 3DS1: Payer Authentication Response. |
cres | string | 3DS2: 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
| Field | Type | Description |
|---|---|---|
version | string | 3DS protocol version ("1.0", "2.1", "2.2"). |
status | string | Authentication outcome: authenticated, attempted, not_authenticated, unavailable, rejected, or not_performed. |
eci | string | Electronic Commerce Indicator. Indicates the level of authentication ("05" = fully authenticated, "06" = attempted, "07" = not authenticated). Exact values vary by card network. |
cavv | string | Cardholder Authentication Verification Value. Cryptographic proof of authentication. |
dsTransId | string | Directory Server Transaction ID. Unique identifier for the 3DS transaction. |
enrolled | string | Whether the card is enrolled in 3DS: "Y" (yes), "N" (no), "U" (unknown). |
liabilityShift | boolean | true if chargeback liability shifted to the issuer. This is the most important field for fraud protection. |
challengeFlow | string | How the authentication was performed: "frictionless" (no customer interaction), "challenge" (customer verified), "decoupled", or "none". |
challenged | boolean | Whether 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"
}
}| Field | Values | Description |
|---|---|---|
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:
| Setting | Default | Description |
|---|---|---|
defaultMode | "auto" | The 3DS mode used when the request has no threeDSPreference.mode. Same values as mode above. |
autoUpgradeOnSoftDecline | true | If a payment is soft-declined because 3DS was not performed, Miracle automatically retries with 3DS enabled. |
attemptNon3ds | true | If 3DS is unavailable (e.g. card not enrolled), proceed with the payment without 3DS. When false, the payment is declined instead. |
requireSca | false | When 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
Related
- HPP Integration — 3DS is handled automatically
- Payment Methods — supported card brands
- Test Cards — sandbox test scenarios including 3DS
- Webhooks Setup — receiving payment results
- Idempotency — safe retries for API calls