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

# Secure Card Display

> Display sensitive card details and PIN in a secure hosted iframe — card data and PIN each require a separate view token

<Warning>
  **Cardholders must set their PIN before card details or PIN reveal can be viewed.**

  If `pin_set` is `false` and you request `["data"]` or `["pin-view"]`, the API returns a `422 PIN_NOT_SET` error with a ready-to-use `pin_setup_url`. Open that URL so the cardholder can set their PIN, then retry the view-token request.

  To **set** the PIN, call with `enabled_views: ["pin-set"]` — this always returns **200 OK** and `secure_view_url`, regardless of `pin_set` status. See [Setting a Card PIN](/yativo-crypto/cards/set-pin).
</Warning>

<Note>
  Card details (PAN, CVV, expiry) and the card PIN each use **separate view tokens**. You must request the correct `enabled_views` for each use case. Never reuse a token between sessions.
</Note>

## How it works

Sensitive card data is never returned in plain API responses. Instead, Yativo hosts a secure card view page and provides you with a short-lived URL to embed in your application:

1. Your backend calls `POST /yativo-card/{yativoCardId}/cards/{cardId}/view-token`
2. If you request `["data"]` or `["pin-view"]` and `pin_set` is `false`, the API returns `PIN_NOT_SET` and a `pin_setup_url` — direct the cardholder there to set their PIN first. If you request `["pin-set"]`, you always receive a `secure_view_url` regardless of `pin_set` status.
3. Yativo returns a `secure_view_url` — a hosted, tokenized page
4. Your frontend embeds that URL in an `<iframe>`
5. The hosted page renders the sensitive data directly — your application never touches the raw values

<Warning>
  View tokens expire quickly. Always request a fresh token immediately before rendering and never cache or log `secure_view_url`.
</Warning>

***

## Step 1 — Request a view token

Call this from your **backend**. The token is scoped to the views you request.

```
POST /yativo-card/{yativoCardId}/cards/{cardId}/view-token
```

<ParamField path="yativoCardId" type="string" required>
  The Yativo Card account ID (`yativo_card_id`) returned during onboarding.
</ParamField>

<ParamField path="cardId" type="string" required>
  The card ID from card creation.
</ParamField>

<ParamField body="enabled_views" type="array">
  Which views to enable on the hosted page. Accepted values:

  | Value | Tab shown | Requires `pin_set: true`? |
  | - | - | - |
  | `"data"` | Card number, CVV, expiry | Yes — returns `422 PIN_NOT_SET` if PIN not set |
  | `"pin-view"` | Reveal current PIN | Yes — returns `422 PIN_NOT_SET` if PIN not set |
  | `"pin-set"` | Set or change PIN | **No** — always returns `200` so the cardholder can set their PIN |

  The legacy value `"pin"` is accepted as an alias for `"pin-set"`.

  Combine freely, e.g. `["data", "pin-view", "pin-set"]`. The page renders one tab per value. If `pin_set` is `false` and `"pin-set"` is **not** in the request, the API returns `422 PIN_NOT_SET`. Including `"pin-set"` in any request bypasses that gate — the token is issued so the cardholder can set their PIN.
</ParamField>

<ParamField body="access_code" type="string">
  Optional extra PIN or unlock code the user must enter before the card details are revealed. Adds an additional layer of protection for high-security flows.
</ParamField>

<ParamField body="theme" type="object">
  Optional per-request theme tokens for the hosted page wrapper. For full CSS control — including card data text colors and fonts — set persistent customization via `PUT /api/card-issuer/pse-customization` (see [Custom CSS](#custom-css) below). All `theme` fields are optional.
</ParamField>

<ParamField body="theme.accent_color" type="string">
  Primary brand color (hex). Default: `#813AE3`
</ParamField>

<ParamField body="theme.background_color" type="string">
  Page background color (hex).
</ParamField>

<ParamField body="theme.panel_color" type="string">
  Card panel background color (hex). The card data area itself always stays light, with dark digits. See [Light card data](#light-card-data).
</ParamField>

<ParamField body="theme.text_color" type="string">
  Primary text color (hex).
</ParamField>

<ParamField body="theme.muted_color" type="string">
  Secondary/muted text color (hex).
</ParamField>

<ParamField body="theme.border_radius" type="number">
  Border radius in pixels.
</ParamField>

<ParamField body="theme.font_family" type="string">
  CSS font-family string (e.g. `"Inter, sans-serif"`).
</ParamField>

<ParamField body="theme.logo_url" type="string">
  URL of your logo to display on the hosted page.
</ParamField>

<RequestExample>
  ```bash Card details only theme={null}
  curl -X POST 'https://crypto-api.yativo.com/api/v1/yativo-card/yc_01HX9KZMB3F7VNQP8R2WDGT4E5/cards/card_01HX9KZMB3F7VNQP8R2WDGT4E5/view-token' \
    -H 'Authorization: Bearer YOUR_API_KEY' \
    -H 'Content-Type: application/json' \
    -d '{
      "enabled_views": ["data"]
    }'
  ```

  ```bash Card details + view PIN theme={null}
  curl -X POST 'https://crypto-api.yativo.com/api/v1/yativo-card/yc_01HX9KZMB3F7VNQP8R2WDGT4E5/cards/card_01HX9KZMB3F7VNQP8R2WDGT4E5/view-token' \
    -H 'Authorization: Bearer YOUR_API_KEY' \
    -H 'Content-Type: application/json' \
    -d '{
      "enabled_views": ["data", "pin-view"]
    }'
  ```

  ```bash Card details + set/change PIN theme={null}
  curl -X POST 'https://crypto-api.yativo.com/api/v1/yativo-card/yc_01HX9KZMB3F7VNQP8R2WDGT4E5/cards/card_01HX9KZMB3F7VNQP8R2WDGT4E5/view-token' \
    -H 'Authorization: Bearer YOUR_API_KEY' \
    -H 'Content-Type: application/json' \
    -d '{
      "enabled_views": ["data", "pin-set"]
    }'
  ```

  ```bash All three tabs + access code theme={null}
  curl -X POST 'https://crypto-api.yativo.com/api/v1/yativo-card/yc_01HX9KZMB3F7VNQP8R2WDGT4E5/cards/card_01HX9KZMB3F7VNQP8R2WDGT4E5/view-token' \
    -H 'Authorization: Bearer YOUR_API_KEY' \
    -H 'Content-Type: application/json' \
    -d '{
      "enabled_views": ["data", "pin-view", "pin-set"],
      "access_code": "1234",
      "theme": {
        "accent_color": "#0f766e",
        "background_color": "#f5f1ea",
        "border_radius": 24,
        "font_family": "Inter, sans-serif",
        "logo_url": "https://yourapp.com/logo.png"
      }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 200 PIN already set theme={null}
  {
    "success": true,
    "data": {
      "secure_view_url": "https://crypto-api.yativo.com/api/yativo-card/view/eyJhbGciOiJIUzI1NiIs...",
      "expires_at": "2026-03-26T12:01:00Z",
      "last_four": "4242",
      "card_type": "virtual",
      "holder_name": "Jane Doe",
      "pin_set": true,
      "enabled_views": ["data", "pin-view", "pin-set"],
      "requires_access_code": false
    }
  }
  ```

  ```json 422 PIN not set theme={null}
  {
    "success": false,
    "error_code": "PIN_NOT_SET",
    "message": "The cardholder has not set a card PIN yet. Direct them to pin_setup_url to set their PIN before accessing card details.",
    "data": {
      "pin_setup_url": "https://crypto-api.yativo.com/api/yativo-card/view/eyJhbGciOiJIUzI1NiIs...",
      "expires_at": "2026-03-26T12:01:00Z",
      "last_four": "4242",
      "card_type": "virtual",
      "holder_name": "Jane Doe",
      "pin_set": false
    }
  }
  ```
</ResponseExample>

<Accordion title="Response Type Definitions">
  ```typescript theme={null}
  interface ViewTokenSuccess {
    secure_view_url: string;          // Hosted page URL — embed this in an iframe
    expires_at: string;               // ISO 8601 — token is invalid after this
    last_four: string;                // Last 4 digits — safe to display without the iframe
    card_type: string;                // "virtual" | "physical"
    holder_name: string | null;       // Cardholder display name
    pin_set: boolean;                 // Always true when this response is returned
    enabled_views: string[];          // Which views are active — any combination of "data" | "pin-view" | "pin-set"
    requires_access_code: boolean;    // True if access_code was set; user must enter it before details are shown
  }

  interface ViewTokenPinNotSet {      // Returned when pin_set is false AND enabled_views does NOT include "pin-set" (HTTP 422)
    pin_setup_url: string;            // Hosted PIN setup page — direct the cardholder here
    expires_at: string;
    last_four: string;
    card_type: string;
    holder_name: string | null;
    pin_set: false;
  }
  ```
</Accordion>

***

## Step 2 — Embed the iframe

Pass the `secure_view_url` directly as the `src` of an iframe. No SDK or additional JavaScript is required.

```html theme={null}
<iframe
  src="https://crypto-api.yativo.com/api/v1/card-view?token=eyJhbGciOiJIUzI1NiIs..."
  title="Secure Card View"
  width="420"
  height="740"
  style="border: 0; border-radius: 16px;"
  allow="clipboard-read; clipboard-write"
></iframe>
```

<Tip>
  The hosted page is fully responsive. Recommended dimensions: `420 × 740px` for card details, `420 × 520px` for PIN-only views.
</Tip>

The page can be embedded on any website or web app. Access is controlled by the short-lived signed token in the URL, not by the embedding site.

***

## Step 3 — Backend proxy (required)

Your frontend must **never** call the Yativo API directly with your credentials. Always proxy the token request through your backend:

<Tabs>
  <Tab title="Express.js">
    ```typescript theme={null}
    import express from 'express';

    const app = express();
    app.use(express.json());

    app.post('/api/card-view-token', async (req, res) => {
      // 1. Authenticate your own user first
      const user = await authenticateRequest(req);
      if (!user) return res.status(401).json({ error: 'Unauthorized' });

      const { enabled_views, access_code, theme } = req.body;

      // 2. Validate enabled_views — only accept known values
      const VALID_VIEWS = ['data', 'pin-view', 'pin-set'];
      if (enabled_views && !enabled_views.every(v => VALID_VIEWS.includes(v))) {
        return res.status(400).json({ error: 'Invalid enabled_views' });
      }

      // 3. Look up this user's card IDs from your own database
      const { yativoCardId, cardId } = await getUserCardIds(user.id);

      // 4. Call Yativo from your backend — credentials never leave the server
      const response = await fetch(
        `https://crypto-api.yativo.com/api/v1/yativo-card/${yativoCardId}/cards/${cardId}/view-token`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${process.env.YATIVO_API_KEY}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            ...(enabled_views ? { enabled_views } : {}),
            ...(access_code ? { access_code } : {}),
            ...(theme ? { theme } : {}),
          }),
        }
      );

      const payload = await response.json();

      // PIN not yet set — return the setup URL so your frontend can redirect the cardholder
      if (!response.ok && payload.error_code === 'PIN_NOT_SET') {
        return res.status(422).json({
          error_code: 'PIN_NOT_SET',
          message: payload.message,
          pin_setup_url: payload.data?.pin_setup_url,
        });
      }

      if (!response.ok) {
        return res.status(502).json({ error: 'Failed to get view token' });
      }

      // Return only the URL — do not cache it
      res.json(payload.data);
    });
    ```
  </Tab>

  <Tab title="Frontend (after fetch)">
    ```html theme={null}
    <!-- Your frontend fetches the URL from your backend, then sets it on the iframe -->
    <div id="card-view-wrapper">
      <iframe id="card-iframe" title="Secure Card View" width="420" height="740"
        style="border:0;border-radius:16px;" allow="clipboard-read; clipboard-write">
      </iframe>
    </div>

    <script>
      async function showCardDetails() {
        // Fetch from YOUR backend — never call Yativo directly from the browser
        const res = await fetch('/api/card-view-token', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ enabled_views: ['data', 'pin'] }),
        });

        if (!res.ok) {
          console.error('Failed to get card view token');
          return;
        }

        const { secure_view_url } = await res.json();
        document.getElementById('card-iframe').src = secure_view_url;
      }

      showCardDetails();
    </script>
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={null}
    import { useEffect, useRef, useState } from 'react';

    interface CardViewProps {
      enabledViews?: ('data' | 'pin-view' | 'pin-set')[];
      height?: number;
    }

    export function SecureCardDisplay({ enabledViews = ['data', 'pin'], height = 740 }: CardViewProps) {
      const [url, setUrl] = useState<string | null>(null);
      const [error, setError] = useState<string | null>(null);

      useEffect(() => {
        let cancelled = false;

        async function fetchToken() {
          try {
            const res = await fetch('/api/card-view-token', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ enabled_views: enabledViews }),
            });

            if (!res.ok) throw new Error('Failed to fetch token');
            const data = await res.json();
            if (!cancelled) setUrl(data.secure_view_url);
          } catch (err) {
            if (!cancelled) setError('Could not load card view. Please try again.');
          }
        }

        fetchToken();
        return () => { cancelled = true; };
      }, [enabledViews.join(',')]);

      if (error) return <p style={{ color: 'red' }}>{error}</p>;
      if (!url) return <div style={{ height, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>Loading...</div>;

      return (
        <iframe
          src={url}
          title="Secure Card View"
          width={420}
          height={height}
          style={{ border: 0, borderRadius: 16 }}
          allow="clipboard-read; clipboard-write"
        />
      );
    }
    ```
  </Tab>
</Tabs>

***

## View options reference

| Value | Tab label | What it renders |
| - | - | - |
| `"data"` | Card Details | Card number (PAN), CVV, expiry |
| `"pin-view"` | View PIN | Reveals the cardholder's current PIN |
| `"pin-set"` | Change PIN | Form to set or change the PIN |

Pass an array with any combination:

* `["data"]` — card details only
* `["pin-set"]` — PIN setup only (e.g. initial onboarding)
* `["data", "pin-view"]` — card details + reveal PIN
* `["data", "pin-set"]` — card details + change PIN
* `["data", "pin-view", "pin-set"]` — all three tabs

<Note>
  `pin_set` reflects the last known state from card activity webhooks. If `pin_set` is `false` and you request `["data"]` or `["pin-view"]`, the API returns a `PIN_NOT_SET` error with a `pin_setup_url` — direct the cardholder there to set their PIN, then retry once the webhook confirms.

  Requesting `["pin-set"]` always returns a `secure_view_url` (HTTP 200) regardless of `pin_set` — use this to present the PIN setup screen without checking `pin_set` first.
</Note>

***

## Access code flow

When you pass `access_code` in the token request, `requires_access_code: true` is returned. The hosted page will prompt the user to enter that code before revealing any card details. Use this for:

* Confirming user intent before showing the full card number
* Adding a secondary verification step in high-value contexts

```bash theme={null}
# Request with access code
curl -X POST '.../view-token' \
  -d '{ "enabled_views": ["data"], "access_code": "7291" }'

# Your user will see a prompt to enter 7291 before card details are shown
```

***

## Customization

<a id="custom-css" />

The secure view page supports two levels of customization: per-request theme tokens and persistent CSS overrides.

### Theme tokens (per request)

Pass a `theme` object in the view-token request to apply brand colors, fonts, and a logo. These take effect immediately on every URL you generate.

| Field | What it affects | Default |
| - | - | - |
| `accent_color` | Active tab, status indicators, spinner, eyebrow label | `#0f766e` |
| `background_color` | Page background | `#f5f1ea` |
| `panel_color` | Hero header, tab strip, and iframe frame background | `#fffaf7` |
| `text_color` | Primary text | `#1e2a24` |
| `muted_color` | Inactive tab labels, secondary text | `#6b746e` |
| `border_radius` | All rounded corners in px (clamped 8–36) | `24` |
| `font_family` | Font for the page shell | system sans-serif |
| `logo_url` | Logo displayed in the header alongside the cardholder name | none |

All fields are optional. Unset fields fall back to the defaults above.

```bash theme={null}
curl -X POST '.../view-token' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "enabled_views": ["data", "pin-view", "pin-set"],
    "theme": {
      "accent_color": "#6366f1",
      "background_color": "#f8f8fc",
      "panel_color": "#ffffff",
      "border_radius": 16,
      "font_family": "Inter, sans-serif",
      "logo_url": "https://yourapp.com/logo.png"
    }
  }'
```

### Custom CSS (persistent, issuer-level)

For fine-grained control over colors, typography, and spacing, you can supply raw CSS that is stored against your issuer account and applied to every secure view URL you generate.

The page has two layers — your CSS affects them differently:

**Layer 1 — Page shell** (injected directly into the page `<style>` block)

The Yativo-rendered wrapper: background, header, tabs, status bar, frame container. All standard CSS applies normally.

**Layer 2 — Card data area** (forwarded to the secure card SDK)

The card number, expiry, and CVV are rendered inside a cross-origin secure iframe. Yativo extracts a specific set of selectors from your stylesheet and forwards them to the secure card SDK, which applies them to the card data elements. Background colors, borders, label fonts, and copy-button styles apply reliably. **Card digit text color** can't be changed: card data always renders dark on a light background.

<Warning>
  For security, the following patterns are stripped before storage: `@import`, external `url()` references, `expression()`, and `behavior:`. Inline data URIs for images (`data:image/png;base64,...`) are allowed.
</Warning>

#### Supported selectors

**Page shell**

| Selector | What it styles |
| - | - |
| `body` | Page background |
| `.panel` | Outer layout wrapper (no visual style by default) |
| `.hero` | Header card (background, border, shadow) |
| `.eyebrow` | "Secure Card View" label |
| `.tab`, `.tab.active` | Tab strip buttons (Card Details / View PIN / Change PIN) |
| `.frame` | Container around the secure card data area |
| `.status`, `.status.ready`, `.status.error` | Status bar text |
| `.footer` | Countdown timer |

**Card data area**

| Selector | What it overrides |
| - | - |
| `.pse-container` | Card data container (background, border, border-radius) |
| `.pse-card-field` | Individual field rows |
| `.pse-card-label` | Field labels ("CARD NUMBER", "EXPIRES", "CVV") |
| `.pse-card-value` | Field value layout (background, font-size, padding). Digits always render dark on a light background — see [Light card data](#light-card-data). |
| `.pse-copy-button` | Copy-to-clipboard button (background, border, `color`) |

#### Manage your customization

```bash theme={null}
# Set
curl -X PUT 'https://crypto-api.yativo.com/api/v1/card-issuer/pse-customization' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{ "custom_css": "body { background: #f0f0f5; } .hero { background: #ffffff; } ..." }'

# Retrieve
curl 'https://crypto-api.yativo.com/api/v1/card-issuer/pse-customization' \
  -H 'Authorization: Bearer YOUR_API_KEY'

# Clear (revert to defaults)
curl -X PUT 'https://crypto-api.yativo.com/api/v1/card-issuer/pse-customization' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{ "custom_css": "" }'
```

#### Example: brand colours + Inter font

```css theme={null}
/* ── Page shell ── */
body  { background: #f8f8fc; }
.hero { background: #ffffff; border-color: rgba(0,0,0,0.08); }
.tab  { color: #6b7280; background: #ffffff; border-color: rgba(0,0,0,0.08); }
.tab.active { background: #6366f1; border-color: #6366f1; color: #ffffff; }
.frame { background: #ffffff; border-color: rgba(0,0,0,0.08); }
.status.ready { color: #6366f1; }
.footer { color: #9ca3af; }

/* ── Card data area ── */
.pse-container  { background-color: #ffffff; border: 1px solid rgba(0,0,0,0.08); border-radius: 12px; }
.pse-card-field { background-color: #ffffff; border-bottom-color: rgba(0,0,0,0.06); }
.pse-card-label { color: #9ca3af; font-size: 11px; font-family: Inter, sans-serif; letter-spacing: 0.05em; text-transform: uppercase; }
.pse-card-value { font-size: 14px; font-family: Inter, sans-serif; }
.pse-copy-button { color: #374151; background: transparent; border-color: rgba(0,0,0,0.12); }
```

### Light card data

The card number, expiry, CVV and PIN always render on a white background with dark digits. This stays the same on every device, including phones in dark mode, so card details are always legible. Your theme colors, font and logo still style the page around the card data.

### Styling notes

<Warning>
  **Copy button icon color cannot be changed.** The clipboard SVG inside `.pse-copy-button` has a hardcoded fill. Setting `color` on `.pse-copy-button` affects the button border and background only — not the icon itself.
</Warning>

<Warning>
  **View PIN and Change PIN are always light-mode.** These tabs are rendered entirely by the card network's secure iframe and cannot be themed via custom CSS.
</Warning>

***

<CardGroup cols={2}>
  <Card title="Virtual Cards" icon="credit-card" href="/yativo-crypto/cards/virtual-cards">
    Create and manage virtual cards.
  </Card>

  <Card title="API Reference — View Token" icon="key" href="/api-reference/cards/customer-secure-view">
    Full parameter reference for the view-token endpoint.
  </Card>
</CardGroup>


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