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

# Secure Elements Overview

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

<Warning>
  **Staging only, reveal only.** Secure Elements is under active development.
  Everything on this page and the [Card Reveal](/build-a-platform/card-reveal)
  page runs against Fluz's staging environment — production hosts aren't
  confirmed yet. This section documents the **Card Reveal** capability only;
  collecting a physical card (tokenization) isn't covered here yet.
</Warning>

## 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 card data; your page and your servers only ever hold a short-lived, opaque token that authorizes one specific action.

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 scope granted only to developers who've demonstrated PCI DSS compliance. (Personal/private applications are exempt.)     |
| [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. |

<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 a user their PAN, expiry, and CVV 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.
  </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 and has the `CREATE_VIRTUALCARD` scope enabled on your access token.
* You have an `ACTIVE` virtual card id, owned by the account you're revealing, to pass when minting a client token.

<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`         |

<Note>
  Reveal currently returns simulated results on staging while Fluz's processor
  integration is finalized. Use staging to validate your integration end to end
  — production availability will be confirmed separately.
</Note>

`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-staging.fluz.app/secure-elements/v0.1.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.

<Info>
  Only the staging CDN host is live so far (`secure-cdn-staging.fluz.app`).
  Production hosting will be confirmed alongside production API availability.
</Info>

## 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>"
}
```

```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 for every reveal. `clientToken` is what authorizes the reveal itself (`expiresIn` seconds, 300 by default); `loadToken` is scoped even tighter (60 seconds) since it travels in a URL — see the note in [Card Reveal](/build-a-platform/card-reveal) — and is rejected everywhere except loading the frame. Pass both straight into `createCardViewer`, 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                                  |
  | 401    | `unauthorized`             | missing access token                                                        |
  | 401    | `invalid_token`            | access token failed verification (malformed, expired, wrong signature)      |
  | 403    | `insufficient_scope`       | access token missing the required scope                                     |
  | 403    | `app_not_registered`       | your application isn't registered — contact Fluz                            |
  | 403    | `forbidden`                | card 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` accepts an optional `style` object, applied to every field it mounts:

```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                                 |

`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:

```text theme={null}
frame-src https://staging.secure.fluz.app;  # or https://secure.fluz.app in production
```

## 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 the Fluz-hosted frame, isolated from your page's own JavaScript. Each configured field mounts as its own sandboxed (`allow-scripts allow-same-origin allow-forms`), `referrerPolicy="no-referrer"` iframe — the SDK never puts card data in the DOM outside of them.
* The frame only renders inside origins you've pre-registered with Fluz.

## 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="Live demo" icon="play" href="https://demo.secure.fluz.app/">
    See the card viewer running against staging, including reveal,
    reveal-CVV-only, and mask.
  </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>
