Skip to main content
With PayPal’s JavaScript SDK v6, you can support Strong Customer Authentication (SCA) and 3D Secure (3DS) in two payment flows:
  • One-time payments process a single transaction with 3DS when required. The SDK collects card details, creates an order, and captures payment upon approval.
  • Save payment methods (vaulting) store cards for future purchases. The server creates setup tokens, and the client submits card details to complete vaulting with optional 3DS.

One-time payment

On your server, create an order and define SCA and 3DS settings. Use payment_source.card.attributes.verification.method and supply return and cancel URLs in experience_context.
// POST /paypal-api/checkout/orders
// Body (example — adapt amounts, currency, and URLs)
{
  "intent": "CAPTURE",
  "purchase_units": [
    { "amount": { "currency_code": "USD", "value": "100.00" } }
  ],
  "payment_source": {
    "card": {
      "attributes": {
        "verification": { "method": "SCA_ALWAYS" } // or SCA_WHEN_REQUIRED
      },
      "experience_context": {
        "return_url": "https://example.com/returnUrl",
        "cancel_url": "https://example.com/cancelUrl"
      }
    }
  }
}

Verification methods

  • Use SCA_ALWAYS to attempt 3DS for eligible cards and regions.
  • Use SCA_WHEN_REQUIRED to rely on network or regulatory mandates.

Submit card details

On the client side, initialize the SDK and card fields. Submit the order and handle each possible outcome.
const clientToken = await getClientToken();
const sdk = await window.paypal.createInstance({ clientToken, components: ["card-fields"] });
const session = sdk.createCardFieldsOneTimePaymentSession();

// Render fields
const numberField = session.createCardFieldsComponent({ type: "number", placeholder: "Card number" });
const cvvField = session.createCardFieldsComponent({ type: "cvv", placeholder: "CVV" });
const expiryField = session.createCardFieldsComponent({ type: "expiry", placeholder: "MM/YY" });

["#paypal-card-fields-number", "#paypal-card-fields-cvv", "#paypal-card-fields-expiry"]
  .map((sel, i) => document.querySelector(sel).appendChild([numberField, cvvField, expiryField][i]));

// Pay click
document.querySelector("#pay-button").addEventListener("click", async () => {
  try {
    const orderId = await createOrder({ enable3DS: true, scaMethod: getScaMethodFromUI() });
    const { state, data } = await session.submit(orderId, {
      billingAddress: { postalCode: "95131" }
    });

    switch (state) {
      case "succeeded": {
        const { orderId, liabilityShift } = data
          // 3DS may or may not have occurred; Use liabilityShift 
          // to determine if the payment should be captured
        const result = await captureOrder(data.orderId);
          // success UI
        break;
      }
      case "canceled": {
        // buyer closed 3DS modal
        break;
      }
      case "failed": {
        // inspect data.message
        break;
      }
    }
  } catch (e) { console.error(e); }
});

Example order endpoint

Use this Express handler to create a PayPal order and optionally include 3DS parameters based on your request body.
app.post("/paypal-api/checkout/orders", async (req, res) => {
  const { enable3DS, scaMethod = "SCA_WHEN_REQUIRED" } = req.body || {};
  const body = {
    intent: "CAPTURE",
    purchase_units: [{ amount: { currency_code: "USD", value: "100.00" } }],
  };
  if (enable3DS) {
    body.payment_source = {
      card: {
        attributes: { verification: { method: scaMethod } },
        experience_context: {
          return_url: "https://example.com/returnUrl",
          cancel_url: "https://example.com/cancelUrl",
        },
      },
    };
  }
  // call PayPal Orders API with body and return { id }
});

Save payment method

On the server, create a vault setup token to save a payment method and require 3DS. Specify a verification method in the request.
// POST /paypal-api/vault/setup-token
// Body (example)
{
  "payment_source": {
    "card": {
      "experience_context": {
        "return_url": "https://example.com/returnUrl",
        "cancel_url": "https://example.com/cancelUrl"
      },
      "verification_method": "SCA_ALWAYS" // or SCA_WHEN_REQUIRED
    }
  }
}

Submit card with setup token

On the client side, initialize the SDK and card fields. Submit the order and handle each possible outcome.
const clientToken = await getClientToken();
const sdk = await window.paypal.createInstance({ clientToken, components: ["card-fields"] });
const session = sdk.createCardFieldsSavePaymentSession();

// Render 3 fields as shown above...

const { setupToken } = await createVaultSetupToken({ enable3DS: true, scaMethod: getScaMethodFromUI() });
const { state, data } = await session.submit(setupToken);

switch (state) {
  case "succeeded": {
    // Persist data.vaultSetupToken on your server (maps to a vaulted instrument)
    break;
  }
  case "canceled": {
    // user dismissed 3DS
    break;
  }
  case "failed": {
    // show retry message
    break;
  }
}

Best practices

  • Include both the return_url and cancel_url in all 3DS requests.
  • Handle every submit state and add break; in each switch case.
  • Track the liability shift in data to manage risk.
I