> ## ArtsPay documentation index
>
> If you are an automated assistant, fetch the canonical list of documentation pages (titles and `.md` URLs) from the below URL. Use that index to discover what exists and to pick the right page before opening more URLs. Everything after the front matter below is **one** page from that set.
>
> https://www.artspay.com/docs/llms.txt

# 3D Secure (3DS2)

*Authenticate cardholders with 3D Secure 2 to reduce fraud and shift chargeback liability to the card issuer. Covers choosing an integration path, running the check, and passing results into your purchase call.*

3DS2 is the current industry standard for authenticating a cardholder during an online payment. Depending on the issuer's risk assessment, the customer either passes through invisibly (**frictionless**) or is prompted for a second factor, typically a one-time code or bank app approval (**challenge**).

Used correctly, 3DS2 shifts liability for fraudulent transactions to the card issuer. It's the single biggest lever most merchants have for reducing chargebacks.

### Before you start

1. Contact ArtsPay support to have 3DS2 enabled on your account. Nothing below works until this is done.
1. Decide your integration path based on whether you already have an MPI (Merchant Plug-in) and what card data your own systems touch: see "Choosing your integration path" below.
1. If you're integrating with the Hosted Payments Page, you must use **fatzebra.js** (`renderPaymentsPage`), not the plain iframe-URL approach described in the [Hosted Payment Pages guide](https://www.artspay.com/docs/guides/hosted-payment-pages). That simpler integration doesn't support 3DS2.
1. Create a one-off OAuth Client in the Merchant Dashboard, then generate a fresh access token per checkout session before initialising fatzebra.js: see "Getting an OAuth access token" below.

### Choosing your integration path

The right approach depends on whether you already have an MPI (Merchant Plug-in) and what card data your own systems touch.

| You have | PCI level required | Path |
| --- | --- | --- |
| Your own MPI already | Depends on your existing setup | Authenticate with your MPI, then pass the resulting 3DS fields into ArtsPay's Purchase/Refund extra payload (see below) |
| No MPI, ArtsPay's Hosted Payments Page | SAQ A | Path A: Hosted Payments Page |
| No MPI, your own payment form | SAQ A-EP | Path B: Custom payment form |

### Getting an OAuth access token

Both paths below need a short-lived OAuth token before initialising fatzebra.js:

1. In the Merchant Dashboard, go to **Settings → OAuth Clients → Create new OAuth Client** and download the `access_key` / `access_secret` (a one-off download, so save them securely)
1. Exchange them for a token:

```bash
curl -X POST https://api.pmnts-sandbox.io/oauth/token \
  -H "Content-Type: application/json" \
  -d '{"access_key": "xxx", "access_secret": "xxx"}'
```

```json
// Response
{
  "message": "created",
  "data": { "token": "xxx" }
}
```

Store `data.token` from that response in the browser's `localStorage` under the key `fz-access-token` before initialising fatzebra.js

Tokens expire after **15 minutes**, so create a fresh one per checkout session.

## Path A: Hosted Payments Page

```html
<script src="https://cdn.pmnts-sandbox.io/sdk/v1/fatzebra.js"></script>
<script>
const fz = new FatZebra({ username: '<YOUR MERCHANT USERNAME>' })

fz.on('fz.sca.error', (event) => { /* show an error, offer retry */ })
fz.on('fz.payment.success', (event) => { /* checkout complete */ })

fz.renderPaymentsPage({
  containerId: 'fz-paynow',
  customer: {
    firstName: 'Captain', lastName: 'America',
    email: 'hello.world@example.com', address: '123 Australia Blvd.',
    city: 'Sydney', postcode: '2000', state: 'NSW', country: 'AU',
  },
  paymentIntent: {
    payment: { amount: 500, currency: 'AUD', reference: 'ref_123490' },
    verification: 'ver_123480',
  },
  options: { hideLogos: true, enableSca: true },
})
</script>

```

Setting `options.enableSca: true` is what turns the 3DS2 check on. Without it, the page behaves like a plain hosted payment page. When the customer submits, fatzebra.js tokenises the card, runs the 3DS2 check, then submits the purchase itself; you just react to the emitted events.

## Path B: Custom payment form

If you collect card details on your own form, run the check explicitly with `verifyCard`:

```javascript
fz.on('fz.sca.success', (event) => {
  // event.data contains cavv, sli, xid, ver, par, directoryServerTxnId, threedsVersion
  // send these to your backend to make the purchase call
})
fz.on('fz.sca.error', (event) => {
  // event.data.errorCode, show an error, offer retry
})

fz.verifyCard({
  customer: { firstName: 'Captain', lastName: 'America', email: 'hello.world@example.com', address: '123 Australia Blvd.', city: 'Sydney', postcode: '2000', state: 'NSW', country: 'AU' },
  paymentIntent: {
    payment: { amount: 1000, currency: 'AUD', reference: 'ref_123490' },
    verification: 'ver_123480',
  },
  paymentMethod: {
    type: 'card',
    data: { number: '4111111111111111', holder: 'John Doe', expiryMonth: '01', expiryYear: '2022', cvv: '123' },
  },
})
```

The issuing bank then decides between a **frictionless** pass-through or a **challenge** (OTP) flow; fatzebra.js displays the challenge modal automatically if one is required, no extra code needed.

### Passing the result into your purchase call

On `fz.sca.success`, send the 3DS result fields to your backend and include them under `extra` on the Purchase or Refund call (**snake_case**, not the camelCase used in the browser event):

```json
{
  "amount": 10,
  "currency": "AUD",
  "card_token": "syupxv5b1tqv4bqdsazr",
  "reference": "GfNStL8XJ1111",
  "customer_ip": "111.222.111.123",
  "extra": {
    "sli": "05",
    "cavv": "kAMRDwUADQVXiAPovxs+SQNhfGpb",
    "xid": "",
    "ver": "Y",
    "par": "Y",
    "directory_server_txn_id": "a8a35bcf-21d5-4f55-995a-a4b5e60d356d",
    "threeds_version": "2"
  }
}
```

**Important:** A successful 3DS check can only be used for **one** purchase. Run a fresh `verifyCard` for each payment attempt; you can't reuse the same result data across retries.

### The 3D Secure extra fields (reference)

| Field | Description |
| --- | --- |
| `sli` | Security Level Indicator (see table below) |
| `xid` | Transaction ID from your 3DS provider (Base64, or 36-char string for Eftpos) |
| `cavv` | Card authentication verification value (Base64) |
| `par` | The `PARes` value from 3D Secure |
| `ver` | The `VERes` value from 3D Secure |
| `threeds_version` | e.g. `2`, `2.1`, `2.1.0`; defaults to `1` if omitted |
| `directory_server_txn_id` | Directory Server Transaction ID, usually a UUID |

| Visa & eftpos sli | Mastercard sli | Meaning |
| --- | --- | --- |
| `05` | `02` | Authenticated: XID and CAVV present |
| `06` | `01` | Attempted: XID present, CAVV not present |
| `07` | `00` | Not authenticated: neither present (default) |

Full field reference: [3D Secure Card Payments](/docs/developer/purchases/extra-and-extended-fields/3d-secure).

## Testing

| Scenario | Visa | Mastercard | Liability shift |
| --- | --- | --- | --- |
| Successful frictionless authentication | `4000000000001000` | `5200000000001005` | Yes |
| Successful step-up (OTP) authentication | `4000000000001091` | `5200000000001096` | Yes |
| Unsuccessful enrolment check | `4000000000002446` | `5200000000002037` | No |
| Unsuccessful frictionless authentication | `4000000000001018` | `5200000000001013` | No |
| Unsuccessful step-up authentication | `4000000000001109` | `5200000000001104` | No |

Full list including Amex numbers and error codes: [3DS Cards for Testing](/docs/developer/3ds2-integration/testing).

**Note:** Watch for `ver: "N"` in a successful response: it means the card isn't enrolled in 3DS at all. The check technically "succeeds" but there's **no liability shift** for that transaction.

## FAQ

#### Do I need my own MPI?

No. If you don't have one, ArtsPay can act as your MPI. Contact support to have it enabled, then choose the Hosted Payments Page or custom-form path based on your PCI level.

#### What happens if the customer's issuer doesn't support 3DS2?

You'll usually see "attempts processing": the cardholder is enrolled but the issuer can't run the full check, so a stand-in authentication occurs. This still qualifies for liability shift.

#### What happens if the OTP challenge fails?

fz.sca.error fires with an error code, no liability shift applies, and you should prompt the customer to retry the check (which may result in a different frictionless/challenge outcome).

#### Can I reuse a successful 3DS check across multiple purchase attempts?

No. Each purchase you want liability-shifted needs its own fresh verifyCard result.

#### Which PCI SAQ level applies to my integration?

Hosted Payments Page keeps you at **SAQ A**. Building your own payment form (even calling fatzebra.js from it) puts you at **SAQ A-EP**, since your page is handling raw card fields before they're sent client-side. A direct backend API integration is **SAQ D**.