> ## 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.

# Overview

> Embed a Fluz-hosted frame directly in your page to show or collect card details, without the data ever reaching your servers or your page's JavaScript.

## What Secure Elements is

Secure Elements is a JavaScript SDK, `@fluz/secure-elements`, that mounts an isolated, Fluz-hosted frame directly into a container element on your page. The frame renders or collects sensitive data; your page and your servers only ever hold a short-lived, opaque token that authorizes one specific action. It covers three capabilities:

* **[Card Reveal](/build-a-platform/card-reveal)** — show a user their own PAN, expiry, and CVV.
* **[Secure Card Input](/build-a-platform/secure-card-input)** — collect a physical card and add it as a payment method on their account.
* **[VAN Reveal](/build-a-platform/van-reveal)** — show a user the account and routing number of their own [virtual account number](/features/virtual-account-numbers).

For reveal specifically, this is a third way to show a user their own card details, alongside the two you already have:

| Path | Where the card data is readable | What it takes |
| :- | :- | :- |
| [`revealVirtualCardByVirtualCardId`](/api-reference/mutations/reveal-virtual-card-by-virtual-card-id) | Your own server, in the API response | `PCI_COMPLIANCE`, a Fluz-administered application-level scope. Granted on request for personal or private applications on their own account; on other people's accounts it requires a PCI DSS certification on file. [Due diligence →](/build-a-platform#due-diligence) |
| [Embedded Widget](/developers/widgets) | Fluz's hosted modal, full-screen over your page | Nothing beyond the OAuth grant — Fluz owns the whole surface, including the reveal. |
| **Secure Elements Card Reveal** | A Fluz-hosted frame, mounted inline inside your own layout | A client token minted from your existing OAuth access token. No `PCI_COMPLIANCE` grant needed — your page never receives the data, so it isn't in scope for it. |

The same tradeoff applies to collecting a card: a widget lets Fluz capture the PAN inside its own PCI scope, and [Secure Card Input](/build-a-platform/secure-card-input) gets you the same "never touches your servers" property inline in your own page instead of a modal.

[VAN Reveal](/build-a-platform/van-reveal) follows the same pattern as Card Reveal, but for a [virtual account number](/features/virtual-account-numbers)'s account and routing number instead of a card's PAN, expiry, and CVV.

<Note>
  If you're already using the [Embedded Widget](/developers/widgets) for
  everything, you don't need this. Secure Elements is for headless or API-driven
  integrations that still need to show or collect card details without opening
  the full widget modal or pursuing `PCI_COMPLIANCE`.
</Note>

## How it works

```
Your backend  →  mint client token  →  Your frontend  →  SDK mounts Fluz frame  →  callback with result
```

<Steps>
  <Step title="Your backend mints a client token" icon="server">
    Exchange your existing [Fluz OAuth access
    token](/build-a-platform/oauth-applications-overview) for a short-lived
    **client token**, scoped to a single reveal or a single card-add.
  </Step>

  <Step title="Your frontend mounts the frame" icon="app-window">
    Hand the client token to `@fluz/secure-elements`, which mounts the
    Fluz-hosted frame into a container you provide — inline in your page, not a
    modal.
  </Step>

  <Step title="The SDK reports back via callbacks" icon="reply">
    Your page never reads the raw card data. It only sees success, error, or
    mount events.
  </Step>
</Steps>

## Prerequisites

* Your application is registered with Fluz. The scope required on your access token depends on which capability you're minting a client token for:

  | Purpose | Required scope |
  | :- | :- |
  | `reveal` ([Card Reveal](/build-a-platform/card-reveal)) | `CREATE_VIRTUALCARD` |
  | `tokenization` ([Secure Card Input](/build-a-platform/secure-card-input)) | `MANAGE_PAYMENT` |
  | `van-reveal` ([VAN Reveal](/build-a-platform/van-reveal)) | `LIST_PAYMENT` |

* For a reveal token specifically, you also need an `ACTIVE` virtual card id, owned by the account you're revealing, to pass when minting it.

* For a van-reveal token specifically, you also need a `virtualAccountNumberId`, owned by the account you're revealing, to pass when minting it — the same id [`getSpendAccountVirtualAccountNumbers`](/features/virtual-account-numbers#retrieve-a-users-virtual-account-numbers) returns, and the same `LIST_PAYMENT` scope that call already requires.

<Warning>
  **Ask Fluz to allow-list your origin before you write any code.** The frame refuses to render from an origin Fluz hasn't pre-approved — there's no self-serve toggle for this today, so it's the one prerequisite that can block you if you leave it for later.

  Email [partnerships@fluz.app](mailto:partnerships@fluz.app) (or your account manager, if you have one) with your **application name or ID** and every **origin** you need approved — each `http://localhost:PORT` you develop against, plus your staging and production domains. Fluz allow-lists them on the backend; there's nothing to configure on your side once that's done.
</Warning>

## Environments

| Environment | Base URL |
| :- | :- |
| Staging | `https://staging.secure.fluz.app` |
| Production | `https://secure.fluz.app` |

`frameHostOrigin` is optional on `createCardViewer` — omit it and it defaults to production (`https://secure.fluz.app`). Pass it explicitly to target staging. Only these two exact origins are accepted; anything else throws a `FluzElementsError` (`error.code === "INVALID_FRAME_HOST_ORIGIN"`) as soon as you call `createCardViewer`, before any frame is mounted.

## Loading the SDK

`@fluz/secure-elements` isn't published to npm — load it as a browser global (IIFE) build from Fluz's CDN with a `<script>` tag. It exposes a `FluzSecureElements` global:

```html theme={null}
<script src="https://secure-cdn.fluz.app/secure-elements/v0.3.0/index.global.js"></script>
<script>
  const { createCardViewer } = FluzSecureElements;

  const viewer = createCardViewer({
    /* ... */
  });
</script>
```

Every code sample on this page and on [Card Reveal](/build-a-platform/card-reveal) assumes you've loaded the script tag and destructured what you need off `FluzSecureElements`, as above.

Each release publishes to an immutable, version-pinned path (`.../v0.1.0/index.global.js`) and a floating `.../latest/index.global.js` that always points at the newest release. Pin to a specific version for anything beyond a prototype — `latest` can change under you without notice.

## Mint a client token

Your backend calls this using the Fluz OAuth access token you already obtain through the standard [OAuth grant flow](/client-facing-o-auth-grant-flow). **Never send that access token to the browser** — only the `clientToken` / `loadToken` pair this endpoint returns should reach your frontend.

```text theme={null}
POST {baseUrl}/v1/client-token
Authorization: Bearer <your Fluz OAuth access token>
Content-Type: application/json

{
  "purpose": "reveal",
  "virtualCardId": "<uuid>"
}
```

`purpose` is `"reveal"` (with a `virtualCardId`), `"tokenization"` (no `virtualCardId` — see [Secure Card Input](/build-a-platform/secure-card-input)), or `"van-reveal"` (with a `virtualAccountNumberId` instead of a `virtualCardId` — see [VAN Reveal](/build-a-platform/van-reveal)).

```json theme={null}
{ "clientToken": "<token>", "loadToken": "<token>", "expiresIn": 300 }
```

A successful mint returns `201`. Both tokens are single-purpose and short-lived — mint a new pair every time. `clientToken` is what authorizes the reveal or card-add itself (`expiresIn` seconds — 300 by default for `reveal`, 1800 for `tokenization`); `loadToken` is scoped even tighter (60 seconds) since it travels in a URL and is rejected everywhere except loading the frame. Pass both straight into `createCardViewer` or `renderFieldsForTokenization`, and never put `clientToken` in a URL yourself — the SDK already keeps it out of one.

<Accordion title="Error responses">
  \| Status | Error | Meaning | | :-- | :-- | :-- | | 400 | `invalid_purpose` |
  `purpose` missing or invalid | | 400 | `virtual_card_id_required` | missing
  `virtualCardId` for a reveal token | | 400 | `virtual_account_number_id_required` |
  missing `virtualAccountNumberId` for a van-reveal token | | 401 | `unauthorized` | missing access
  token | | 401 | `invalid_token` | access token failed verification (malformed,
  expired, wrong signature) | | 403 | `insufficient_scope` | access token
  missing the scope required for this `purpose` — see
  [Prerequisites](#prerequisites) | | 403 | `app_not_registered` | your
  application isn't registered — contact Fluz | | 403 | `forbidden` | card or
  virtual account number not found, or not owned by this user | | 429 | `rate_limited` | too many
  client-token requests for this user — see the `Retry-After` header | | 500 |
  `internal_error` | unexpected server error |
</Accordion>

## Styling fields

`createCardViewer`, `createAccountViewer`, and `renderFieldsForTokenization` all accept an optional `style` object, applied to every field they mount:

```js theme={null}
const viewer = createCardViewer({
  clientToken,
  loadToken,
  frameHostOrigin: "https://staging.secure.fluz.app",
  style: {
    color: "#1a1a1a",
    fontSize: "16px",
    fontWeight: "600",
    fontFamily: "Inter",
  },
});
```

`style` is validated before anything is sent to the frame. If a value doesn't match what's documented below, `await viewer.mount(...)` rejects with a `FluzElementsError` (`error.code === "INVALID_STYLE"`) — wrap your `mount()` call in a try/catch if you're accepting configurable style input yourself.

| Property | Accepts |
| :- | :- |
| `color` | A hex color (`#1a1a1a` or `#111`), an `rgb(r, g, b)` value, or a CSS named color (`"slategray"`) |
| `fontSize` | A number followed by `px`, `pt`, `em`, or `rem` (e.g. `"16px"`) |
| `fontWeight` | `"normal"`, `"bold"`, or a multiple of 100 from `"100"` to `"900"` |
| `fontFamily` | An exact system font name or Google Font family name — see below |
| `width` | A number followed by `px`, `%`, `em`, or `rem` (e.g. `"100%"`, `"240px"`) |
| `height` | Same format as `width` (e.g. `"44px"`) |
| `border` | `"none"`, or `"<width>px <style> <color>"` where `<style>` is one of `solid`/`dashed`/`dotted`/`double` and `<color>` follows the same rules as `color` above (e.g. `"1px solid #d0d5dd"`) |
| `backgroundColor` | Same format as `color` above |

`width`, `height`, `border`, and `backgroundColor` style the box around each field — but which element that is differs by capability, since reveal and Secure Card Input mount different DOM shapes:

* **`createCardViewer` / `createAccountViewer` (reveal)** — each field is its own frame holding a single value, rendered as one `inline-block` element, so these four properties apply directly to it.
* **`renderFieldsForTokenization` (Secure Card Input)** — the fields share one frame, each inside its own wrapper box. These properties apply to that wrapper, not the input text itself. See [Box-model styling](/build-a-platform/secure-card-input#box-model-styling-width-height-border-background-color) and [Layout](/build-a-platform/secure-card-input#layout) on the Secure Card Input page for the `layout` option this also unlocks.

`fontFamily` must be an **exact, case-sensitive match** for one of two allowlists:

* **System fonts** — common OS/web-safe stacks (`system-ui`, `-apple-system`, `Helvetica Neue`, `Arial`, `Georgia`, `Menlo`, and the generic `monospace` / `serif` / `sans-serif` keywords, among others). These render immediately, with no network request.
* **Google Fonts** — any family from the Google Fonts catalog (`"Roboto"`, `"Inter"`, `"IBM Plex Mono"`, and so on), passed exactly as Google lists it. The SDK loads the font for you — you don't need a `<link>` tag or `@font-face` rule.

<Note>
  A Google Font is fetched after the field mounts, not bundled up front, so
  there's a brief window on a cold cache where the field renders in the
  browser's fallback font before swapping in your chosen one. A system font has
  no such delay.
</Note>

Both lists are exported if you want to validate a font choice, or build a font picker, yourself:

```js theme={null}
const { SYSTEM_FONTS, GOOGLE_FONTS, isAllowedFontFamily } = FluzSecureElements;

isAllowedFontFamily("Roboto"); // true
isAllowedFontFamily("roboto"); // false -- exact, case-sensitive match required
```

## Content Security Policy

If your page sets a CSP, allow the frame host you're targeting, and the CDN host the SDK loads from:

```text theme={null}
frame-src https://secure.fluz.app;  # or https://staging.secure.fluz.app to test against staging
script-src https://secure-cdn.fluz.app 'nonce-<your-nonce>';  # or a hash, for the inline script that calls the SDK
```

The SDK itself loads from `script-src`'s allowed host; the inline `<script>` block in your own integration code (as in the examples on this page and on [Card Reveal](/build-a-platform/card-reveal), [Secure Card Input](/build-a-platform/secure-card-input), and [VAN Reveal](/build-a-platform/van-reveal)) still needs its own nonce or hash if your CSP doesn't allow `'unsafe-inline'`. If you use a nonce, the value on the tag must match the one in the header exactly:

```html theme={null}
<script nonce="<your-nonce>">
  const { createCardViewer } = FluzSecureElements;
  // ...
</script>
```

## Security model

* Your OAuth access token never leaves your servers.
* The client token your frontend holds is opaque and single-purpose — it carries no card data and can't be replayed for a different card or action.
* Card data is only ever readable inside Fluz-hosted frames, isolated from your page's own JavaScript. Every frame is sandboxed (`allow-scripts allow-same-origin allow-forms`) with `referrerPolicy="no-referrer"` — the SDK never puts card data in the DOM outside of them.
* The frame only renders inside origins you've pre-registered with Fluz.
* Card data never reaches your servers or your page's JavaScript — this is the condition most relevant to SAQ A eligibility under PCI DSS, alongside the other requirements SAQ A sets (e.g. not otherwise storing, processing, or transmitting cardholder data yourself). Your applicable PCI DSS obligations depend on your full cardholder data environment; confirm your scope with your QSA.

## Next steps

<CardGroup cols={2}>
  <Card title="Card Reveal" icon="eye" href="/build-a-platform/card-reveal">
    Create the card viewer, mount it, and control which fields are revealed.
  </Card>

  {" "}

  <Card title="Secure Card Input" icon="credit-card" href="/build-a-platform/secure-card-input">
    Collect a physical card and add it as a payment method.
  </Card>

  {" "}

  <Card title="VAN Reveal" icon="building-columns" href="/build-a-platform/van-reveal">
    Show a user the account and routing number of their own virtual account number.
  </Card>

  {" "}

  <Card title="Live demos" icon="play" href="https://demo.secure.fluz.app/">
    See all three capabilities running against staging.
  </Card>

  {" "}

  <Card title="Example integrations" icon="github" href="https://github.com/fluz-app/secure-elements-examples">
    Runnable plain-HTML and React examples, both calling real staging
    infrastructure.
  </Card>

  <Card title="OAuth applications" icon="handshake" href="/build-a-platform/oauth-applications-overview">
    How to obtain the access token you'll exchange for a client token.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.