> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fluz.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Card Reveal

> Mount a Fluz-hosted viewer in your page to show a user their PAN, expiry, and CVV, without the data touching your servers or your page's JavaScript.

<Note>
  This page assumes you've read [Secure Elements
  Overview](/build-a-platform/secure-elements-overview) — it covers minting a
  client token, loading the SDK, and styling fields, all of which apply here.
</Note>

## See it live

<iframe
  src="https://demo.secure.fluz.app/"
  title="Fluz Secure Elements — Card Reveal demo"
  loading="lazy"
  style={{
width: "100%",
height: "720px",
border: "1px solid #e5e5e5",
borderRadius: "8px",
}}
/>

The demo mints its own token and mounts the viewer automatically. Use **Remint token & remount** if the reveal buttons stop responding, and **Reveal**, **Reveal CVV only**, or **Mask** to try the field-level controls covered below. [Open it in its own tab →](https://demo.secure.fluz.app/)

## Mint a reveal token

Call [`POST /v1/client-token`](/build-a-platform/secure-elements-overview#mint-a-client-token) with `"purpose": "reveal"` and the `virtualCardId` you want to show:

```json theme={null}
{
  "purpose": "reveal",
  "virtualCardId": "c107e50b-10f3-449c-92c0-609d9a8cfa2a"
}
```

Pass the `clientToken` and `loadToken` it returns straight into `createCardViewer` below.

## Create the viewer

```js theme={null}
const viewer = createCardViewer({
  clientToken,
  loadToken,
  frameHostOrigin: "https://staging.secure.fluz.app",
  fields: ["pan", "expiry", { field: "cvv", individualReveal: false }],
});
```

`fields` controls which pieces of the card render, in order — omit it and you get `["pan", "expiry", "cvv"]`. Each entry is either a bare field name or a `{ field, individualReveal }` object; `"pan"` is shorthand for `{ field: "pan", individualReveal: true }`. `pan`, `expiry`, and `cvv` are the only valid field names — anything else throws a `FluzElementsError` (`error.code === "INVALID_FIELD"`) synchronously, from `createCardViewer` itself, before you ever call `mount()`.

| Field    | Masked placeholder, before any reveal | Notes                                                          |
| :------- | :------------------------------------ | :------------------------------------------------------------- |
| `pan`    | `•••• •••• •••• {last4}`              | Last 4 digits come from the card metadata on your client token |
| `expiry` | The real `MM/YYYY` — not masked       | Not treated as sensitive                                       |
| `cvv`    | `•••`                                 | Supports `individualReveal: false` (below)                     |

<Note>
  `individualReveal` defaults to `true` on every field. Setting it to `false` prevents that one field from being revealed on its
  own — see [Reveal a single field](#reveal-a-single-field). It's independent of
  `reveal()`, which always reveals every field regardless of this setting.
</Note>

## Mount it

```js theme={null}
await viewer.mount(document.getElementById("card-viewer"));
```

`mount()` appends one sandboxed iframe per configured field into the container element you pass it — three separate frames for the default `fields`, not one combined frame — and returns a promise that resolves once every frame has completed its handshake. It rejects with a `FluzElementsError` if:

* the `style` you passed to `createCardViewer` fails validation (`error.code === "INVALID_STYLE"`) — see [Styling fields](/build-a-platform/secure-elements-overview#styling-fields)
* a frame doesn't complete its handshake within `mountTimeoutMs` (`error.code === "MOUNT_TIMEOUT"`; defaults to 10 seconds, and is configurable via `createCardViewer({ ..., mountTimeoutMs })`)
* a frame fails to load at all, or this viewer is already mounted (`error.code === "MOUNT_FAILED"`) — each `CardViewer` instance can only be mounted once; create a new one with `createCardViewer` if you need to mount again

## Reveal and mask fields

```js theme={null}
await viewer.reveal(); // fetch and show every configured field at once
await viewer.reveal("cvv"); // fetch and show one field, if that field allows it
viewer.setMask("cvv", true); // re-mask a field that's already been revealed
viewer.setMask("cvv", false); // un-mask it again -- see below
```

* **`reveal(field?)`** — async. Fetches the real value from Fluz and displays it. Called with no argument, it fetches and shows every configured field regardless of `individualReveal`. Called with a field name, it fetches and shows just that field — unless that field was configured with `individualReveal: false`, in which case it rejects with `error.code === "INDIVIDUAL_REVEAL_DISABLED"`. An unmounted viewer, or a field name that isn't one of `fields`, rejects with `MOUNT_FAILED`.
* **`setMask(field, masked, options?)`** — synchronous, not async. It never fetches anything — it only toggles what's currently displayed:
  * `setMask(field, true)` re-masks the field back to its placeholder, whether or not it was ever revealed.
  * `setMask(field, false)` un-masks it — but only shows the real value if `reveal()` already fetched one for that field. Call it before any `reveal()` and the field just stays on its placeholder, since there's no fetched value yet to show.
  * `setMask(field, true, { hidden: true })` blanks the field completely (empty, not even a placeholder) instead of showing dots/last4/expiry. `hidden` only has an effect while `masked` is `true`.
  * There's no bulk "mask all" call — call `setMask` once per field in `fields` if you need to reset the whole viewer.
* **`destroy()`** — tears down every frame and detaches the viewer. Call this on unmount so you don't leak mounted frames when your component goes away.

### Reveal a single field

To reveal just one field on its own (e.g. a "Show CVV" button next to that field), call `reveal(field)` — every field defaults to allowing this, so no config is needed for the common case.

If a field should *never* be revealed on its own, and only ever appear as part of the whole-card `reveal()` call, opt it out with `{ field, individualReveal: false }` in `fields`:

```js theme={null}
const viewer = createCardViewer({
  clientToken,
  loadToken,
  frameHostOrigin: "https://staging.secure.fluz.app",
  fields: ["pan", "expiry", { field: "cvv", individualReveal: false }],
});

await viewer.reveal("cvv"); // rejects — error.code === "INDIVIDUAL_REVEAL_DISABLED"
await viewer.reveal(); // succeeds — reveals pan, expiry, and cvv together
```

`reveal(field)` against an opted-out field rejects with `FluzElementsError` (`code: "INDIVIDUAL_REVEAL_DISABLED"`) without contacting `frame-host`. Either way, `reveal()` with no argument always reveals every mounted field — `individualReveal` has no effect on it.

<Note>
  This is a client-side integration choice, not a server-enforced capability — it controls what your own UI is allowed to trigger, not what data the grant can return. Don't rely on `individualReveal: false` as a security boundary.
</Note>

## Handle events

```js theme={null}
const unsubscribeMount = viewer.onMount(() => {
  // all configured fields have finished rendering
});

const unsubscribeError = viewer.onError((error) => {
  // error.code, error.message
});
```

`onMount` fires once, after every configured field has rendered inside the frame. `onError` fires for problems that happen *inside* an already-mounted frame — a failed `reveal()` or a rate limit — rather than problems with `mount()` or `createCardViewer()` itself, which reject or throw directly instead (see below). Both `onMount` and `onError` return an unsubscribe function.

Every `FluzElementsError` this capability can produce, and where it surfaces:

| Code                         | Where it surfaces                                            | Meaning                                                                                                                           |
| :--------------------------- | :----------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- |
| `INVALID_FIELD`              | Thrown by `createCardViewer`                                 | A `fields` entry isn't `pan`, `expiry`, or `cvv`                                                                                  |
| `INVALID_FRAME_HOST_ORIGIN`  | Thrown by `createCardViewer`                                 | `frameHostOrigin` isn't a recognized Fluz frame host                                                                              |
| `INVALID_STYLE`              | Rejected by `mount()`                                        | A `style` value failed validation                                                                                                 |
| `MOUNT_TIMEOUT`              | Rejected by `mount()`                                        | A field's frame didn't complete its handshake within `mountTimeoutMs`                                                             |
| `MOUNT_FAILED`               | Rejected by `mount()`, or thrown by `reveal()` / `setMask()` | A frame failed to load; the viewer was already mounted; or `reveal()` / `setMask()` was called with an unmounted or unknown field |
| `INDIVIDUAL_REVEAL_DISABLED` | Rejected by `reveal(field)`                                  | That field was configured with `individualReveal: false`                                                                          |
| `FIELD_ERROR`                | Delivered to `onError`                                       | A `reveal()` failed inside the frame after mount (network error, or reveal not yet available)                                     |
| `RATE_LIMITED`               | Delivered to `onError`                                       | Too many reveal attempts for this grant — `error.message` includes the retry-after time                                           |

## Full example

```html theme={null}
<div id="card-viewer"></div>

<script src="https://secure-cdn-staging.fluz.app/secure-elements/v0.1.0/index.global.js"></script>
<script>
  (async () => {
    const { createCardViewer } = FluzSecureElements;

    const res = await fetch("/mint-reveal-token", { method: "POST" });
    const { clientToken, loadToken } = await res.json();

    const viewer = createCardViewer({
      clientToken,
      loadToken,
      frameHostOrigin: "https://staging.secure.fluz.app",
      fields: ["pan", "expiry", { field: "cvv", individualReveal: false }],
      style: {
        fontFamily: "IBM Plex Mono",
        fontSize: "16px",
        color: "#1a1a1a",
      },
    });

    viewer.onError((error) => console.error(error.code, error.message));
    viewer.onMount(() => console.log("card viewer ready"));

    await viewer.mount(document.getElementById("card-viewer"));
  })();
</script>
```

`/mint-reveal-token` is your own backend route — the one that calls `POST /v1/client-token` with your Fluz OAuth access token, as described in [Mint a client token](/build-a-platform/secure-elements-overview#mint-a-client-token).

## Next steps

<CardGroup cols={2}>
  <Card title="Secure Elements Overview" icon="book-open" href="/build-a-platform/secure-elements-overview">
    Token minting, SDK loading, styling, and CSP.
  </Card>

  {" "}

  <Card title="Live demo" icon="play" href="https://demo.secure.fluz.app/">
    Reveal, reveal CVV only, and mask, running against staging.
  </Card>

  <Card title="Example integrations" icon="github" href="https://github.com/fluz-app/secure-elements-examples">
    Runnable plain-HTML and React reveal examples with a token-minting server.
  </Card>
</CardGroup>
