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

# Reveal PIN Widget

> Securely display a cardholder's PIN directly in your application, with an auto-hide countdown.

The Reveal PIN widget allows you to securely display a card's PIN directly in your application. The PIN is fetched, rendered, and auto-hidden entirely inside an isolated Synctera-origin iframe — it is never exposed to your page, never included in any event, and never logged — helping reduce your PCI scope.

This is the Synctera replacement for the legacy Marqeta PIN reveal (`pinReveal`) flow. For the deprecated Marqeta integration, see [Marqeta Widgets](/v2/docs/card-widgets-legacy-marqeta#display-card-pin).

## Step 1: Load the Widget Script

Load the Reveal PIN widget script into your page:

<CodeGroup>
  ```html HTML theme={"system"}
  <script
    type="module"
    src="https://assets.synctera.com/widgets/reveal-pin/v1.0.0/index.js"
  ></script>
  ```

  ```javascript React theme={"system"}
  import { useEffect } from 'react';

  function RevealPin({ widgetToken }) {
    useEffect(() => {
      const script = document.createElement('script');
      script.type = 'module';
      script.src = 'https://assets.synctera.com/widgets/reveal-pin/v1.0.0/index.js';
      document.head.appendChild(script);

      return () => {
        document.head.removeChild(script);
      };
    }, []);

    return (
      <reveal-pin
        token={widgetToken}
        env="production"
      />
    );
  }
  ```
</CodeGroup>

## Step 2: Get a Widget Token

Request a widget token scoped to revealing the card's PIN. Use the `GET_PIN` widget type:

<CodeGroup>
  ```bash curl theme={"system"}
  curl -X GET "https://api.synctera.com/v2/cards/{card_id}/widget_token?widget_type=GET_PIN" \
    -H "Authorization: Bearer {apiKey}" \
    -H "Content-Type: application/json"
  ```

  ```javascript Node.js theme={"system"}
  const response = await fetch(
    `https://api.synctera.com/v2/cards/${cardId}/widget_token?widget_type=GET_PIN`,
    {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
      }
    }
  );

  const { widget_token } = await response.json();
  ```
</CodeGroup>

<Warning>
  Widget tokens expire after **5 minutes** and are scoped to a specific card. Generate a new token on each page load or when the user initiates a new reveal action.
</Warning>

## Step 3: Add the Widget Component

Add the `<reveal-pin>` web component to your page:

```html theme={"system"}
<reveal-pin
  token="your-widget-token"
  env="production"
></reveal-pin>
```

By default the widget renders a placeholder (obscured dots) and waits for a reveal to be triggered — either by the built-in **Show PIN** button (set `show-toggle`) or by calling [`requestPin()`](#public-methods) from your own UI. Once revealed, the PIN displays with a live auto-hide countdown and is cleared automatically when the countdown ends.

***

## Configuration Options

| Property            | Type      | Required | Default      | Description                                                                                                                      |
| ------------------- | --------- | -------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `token`             | `string`  | Yes      | -            | Widget token obtained from the API (`GET_PIN` type)                                                                              |
| `env`               | `string`  | Yes      | -            | Environment: `sandbox` or `production`                                                                                           |
| `theme`             | `string`  | No       | `"default"`  | Theme preset: `"default"` or `"night-shift"`                                                                                     |
| `styles`            | `string`  | No       | `"{}"`       | JSON string of [design tokens](/v2/docs/card-widgets-theming) for visual customization                                           |
| `auto-hide-seconds` | `number`  | No       | `5`          | Seconds the PIN stays visible before it auto-hides, with a live countdown. Set to `0` to disable auto-hide (see PCI note below). |
| `show-toggle`       | `boolean` | No       | `false`      | Renders a built-in **Show PIN** button inline with the obscured dots                                                             |
| `toggle-label`      | `string`  | No       | `"Show PIN"` | Custom label text for the built-in button                                                                                        |
| `custom-labels`     | `string`  | No       | `"{}"`       | JSON string of custom labels                                                                                                     |

### Auto-hide Countdown

The PIN auto-hides after `auto-hide-seconds` (default `5`), mirroring the legacy Marqeta `hidePinTimeout` behavior. This is a PCI safeguard, not just a UX nicety: it limits how long the PIN is on screen.

```html theme={"system"}
<reveal-pin
  token="your-token"
  env="production"
  auto-hide-seconds="10"
></reveal-pin>
```

<Warning>
  Setting `auto-hide-seconds="0"` disables auto-hide entirely: the PIN stays visible with no countdown until the widget is torn down or the PIN is re-requested. This weakens your PCI posture — only disable it deliberately.
</Warning>

### Built-in Button

Set `show-toggle` to render a built-in **Show PIN** button next to the obscured dots. When omitted, you control the reveal yourself by calling [`requestPin()`](#public-methods).

```html theme={"system"}
<reveal-pin
  token="your-token"
  env="production"
  show-toggle
  toggle-label="Reveal PIN"
></reveal-pin>
```

### Custom Labels

Customize the labels displayed in the widget:

```html theme={"system"}
<reveal-pin
  token="your-token"
  env="production"
  custom-labels='{
    "pinLabel": "Card PIN",
    "toggleLabel": "Show PIN",
    "retryButtonText": "Try Again"
  }'
></reveal-pin>
```

Available custom label keys:

* `pinLabel` - Label shown above the PIN (default: `"PIN"`)
* `toggleLabel` - Built-in button label (default: `"Show PIN"`). The dedicated `toggle-label` attribute takes precedence if both are set.
* `retryButtonText` - Error state retry button text (default: `"Try Again"`)

***

## Event Handling

The widget dispatches events for both initialization and PIN reveal outcomes:

* **`load`** — The widget finished initializing and is ready to use.
* **`error`** — The widget failed to initialize (network/CSP failure or handshake timeout).
* **`success`** — PIN retrieved and rendered successfully inside the secure iframe.
* **`failure`** — PIN reveal failed (API error, timeout, etc.).

<Info>
  The Reveal PIN widget does **not** emit a `copy` event. The PIN cannot be copied to the clipboard and text selection is disabled inside the widget — both are intentional PCI safeguards.
</Info>

You can subscribe with `addEventListener` or by assigning the matching callback property:

| Event                  | `addEventListener`                       | Callback property       |
| ---------------------- | ---------------------------------------- | ----------------------- |
| Initialization success | `widget.addEventListener('load', fn)`    | `widget.onLoad = fn`    |
| Initialization failure | `widget.addEventListener('error', fn)`   | `widget.onError = fn`   |
| Reveal success         | `widget.addEventListener('success', fn)` | `widget.onSuccess = fn` |
| Reveal failure         | `widget.addEventListener('failure', fn)` | `widget.onFailure = fn` |

### Load Event

Dispatched once when the widget has finished initializing and is ready to use. Use this to reveal the widget UI only after it's ready, or to dismiss a loading placeholder. Fires exactly once per iframe lifecycle.

| Property     | Type     | Description                        |
| ------------ | -------- | ---------------------------------- |
| `instanceId` | `string` | Unique ID for this widget instance |

```html theme={"system"}
<reveal-pin id="pin-widget" token="your-token" env="production"></reveal-pin>
<script>
  document.getElementById('pin-widget').addEventListener('load', (event) => {
    const { instanceId } = event.detail;
    console.log('Widget ready', instanceId);
  });
</script>
```

### Error Event

Dispatched once when the widget fails to initialize — either because the underlying iframe could not load (network failure, blocked by Content Security Policy, etc.) or because the iframe loaded but never completed its handshake within 5 seconds. This is distinct from the `failure` event, which signals that a PIN reveal API call failed *after* the widget was already running.

| Property       | Type       | Description                                                                                                                                 |
| -------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `error`        | `string`   | Human-readable error message (safe to display to end users)                                                                                 |
| `failedFields` | `string[]` | Logical surfaces that failed to initialize. The Reveal PIN widget has a single display surface, so this is always `['display-controller']`. |
| `instanceId`   | `string`   | Unique ID for this widget instance                                                                                                          |

```html theme={"system"}
<script>
  document.getElementById('pin-widget').addEventListener('error', (event) => {
    const { error, failedFields } = event.detail;
    console.error('Widget initialization failed:', error, failedFields);
    // Show error UI or offer a retry button
  });
</script>
```

### Success Event

Dispatched when the PIN is successfully retrieved and rendered inside the widget's secure iframe. The PIN itself is **never** included in the event — only metadata.

| Property     | Type     | Description                        |
| ------------ | -------- | ---------------------------------- |
| `instanceId` | `string` | Unique ID for this widget instance |

```html theme={"system"}
<reveal-pin id="pin-widget" token="your-token" env="production"></reveal-pin>
<script>
  document.getElementById('pin-widget').addEventListener('success', (event) => {
    const { instanceId } = event.detail;
    console.log('PIN revealed successfully', instanceId);
  });
</script>
```

### Failure Event

Dispatched when the PIN reveal API call fails.

| Property     | Type     | Description                        |
| ------------ | -------- | ---------------------------------- |
| `error`      | `string` | Human-readable error message       |
| `instanceId` | `string` | Unique ID for this widget instance |

```html theme={"system"}
<script>
  document.getElementById('pin-widget').addEventListener('failure', (event) => {
    const { error } = event.detail;
    console.error('PIN reveal failed:', error);
    // Show error UI to user
  });
</script>
```

***

## Public Methods

The widget exposes methods for programmatic control. These are useful when you want to control the reveal behavior externally instead of using the built-in button (`show-toggle`).

| Method         | Description                        |
| -------------- | ---------------------------------- |
| `requestPin()` | Trigger the PIN reveal API call    |
| `refresh()`    | Reset state and re-request the PIN |

```html theme={"system"}
<reveal-pin
  id="pin-widget"
  token="your-token"
  env="production"
></reveal-pin>

<button onclick="document.getElementById('pin-widget').requestPin()">
  Reveal PIN
</button>
```

***

## PCI Considerations

The Reveal PIN widget is designed to keep the PIN out of your PCI scope:

* The PIN is fetched and rendered **entirely inside the Synctera-origin iframe**. It never crosses the iframe boundary to your page.
* The PIN is **never included in any event** (`success` carries only an `instanceId`) and is **never logged**.
* The PIN **cannot be copied** to the clipboard, and text selection is disabled, so it can't be highlighted and copied.
* The PIN **auto-hides** after `auto-hide-seconds` and is cleared from the DOM and memory when hidden.

***

## Complete Example

<CodeGroup>
  ```html Complete HTML Example theme={"system"}
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="utf-8" />
      <title>Reveal PIN</title>
      <script
        type="module"
        src="https://assets.synctera.com/widgets/reveal-pin/v1.0.0/index.js"
      ></script>
    </head>
    <body>
      <h1>Your PIN</h1>

      <div id="error-message" style="color: red; display: none;"></div>

      <reveal-pin
        id="pin-widget"
        env="production"
        auto-hide-seconds="5"
        show-toggle
        toggle-label="Show PIN"
      ></reveal-pin>

      <script>
        async function initializeWidget() {
          try {
            const response = await fetch('/api/pin-widget-token', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({
                card_id: 'your-card-id',
                widget_type: 'GET_PIN'
              })
            });

            const { widget_token } = await response.json();

            const widget = document.getElementById('pin-widget');
            widget.setAttribute('token', widget_token);

            widget.addEventListener('load', (event) => {
              console.log('Widget ready:', event.detail.instanceId);
            });

            widget.addEventListener('error', (event) => {
              const { error, failedFields } = event.detail;
              console.error('Widget failed to initialize:', error, failedFields);
              document.getElementById('error-message').textContent = error;
              document.getElementById('error-message').style.display = 'block';
            });

            widget.addEventListener('success', (event) => {
              console.log('PIN revealed:', event.detail.instanceId);
            });

            widget.addEventListener('failure', (event) => {
              document.getElementById('error-message').textContent = event.detail.error;
              document.getElementById('error-message').style.display = 'block';
            });

          } catch (error) {
            console.error('Failed to initialize widget:', error);
          }
        }

        window.addEventListener('DOMContentLoaded', initializeWidget);
      </script>
    </body>
  </html>
  ```

  ```javascript React Example theme={"system"}
  import { useEffect, useState, useRef } from 'react';

  function RevealPinView({ cardId }) {
    const [widgetToken, setWidgetToken] = useState(null);
    const [error, setError] = useState(null);
    const widgetRef = useRef(null);

    useEffect(() => {
      const script = document.createElement('script');
      script.type = 'module';
      script.src = 'https://assets.synctera.com/widgets/reveal-pin/v1.0.0/index.js';
      document.head.appendChild(script);

      fetch('/api/pin-widget-token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ card_id: cardId, widget_type: 'GET_PIN' })
      })
        .then(res => res.json())
        .then(data => setWidgetToken(data.widget_token))
        .catch(err => setError(err.message));

      return () => {
        document.head.removeChild(script);
      };
    }, [cardId]);

    useEffect(() => {
      if (!widgetToken || !widgetRef.current) return;

      const handleLoad = (event) => {
        console.log('Widget ready:', event.detail.instanceId);
      };

      const handleError = (event) => {
        const { error: initError, failedFields } = event.detail;
        console.error('Widget failed to initialize:', initError, failedFields);
        setError(initError);
      };

      const handleSuccess = (event) => {
        console.log('PIN revealed:', event.detail.instanceId);
      };

      const handleFailure = (event) => {
        setError(event.detail.error);
      };

      const widget = widgetRef.current;
      widget.addEventListener('load', handleLoad);
      widget.addEventListener('error', handleError);
      widget.addEventListener('success', handleSuccess);
      widget.addEventListener('failure', handleFailure);

      return () => {
        widget.removeEventListener('load', handleLoad);
        widget.removeEventListener('error', handleError);
        widget.removeEventListener('success', handleSuccess);
        widget.removeEventListener('failure', handleFailure);
      };
    }, [widgetToken]);

    if (error) return <div style={{ color: 'red' }}>Error: {error}</div>;
    if (!widgetToken) return <div>Loading...</div>;

    return (
      <reveal-pin
        ref={widgetRef}
        token={widgetToken}
        env="production"
        auto-hide-seconds="5"
        show-toggle
      />
    );
  }

  export default RevealPinView;
  ```
</CodeGroup>

***

## Migration from Marqeta PIN Reveal

If you previously displayed the PIN using the Marqeta.js `pinReveal` component, the Reveal PIN widget is the modern, Synctera-hosted replacement.

| Marqeta (Old)                                                                    | Synctera (New)                                             |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `marqeta.min.js` + `window.marqeta.bootstrap({ component: { pinReveal: ... } })` | `<reveal-pin>` web component                               |
| `POST /v0/cards/{card_id}/client_token`                                          | `GET /v2/cards/{card_id}/widget_token?widget_type=GET_PIN` |
| `cardholderVerificationMethod: 'OTHER'` (manual config)                          | Handled internally — no configuration needed               |
| `hidePinTimeout` (5–15s)                                                         | `auto-hide-seconds` (default `5`)                          |
| `toggleCardPin`                                                                  | `show-toggle` / `requestPin()`                             |
| `onSuccess` / `onFailure` callbacks                                              | `load` / `error` / `success` / `failure` DOM events        |

See the [Migration Guide](/v2/docs/card-widgets-legacy-migration) for the Activate Card and Set PIN equivalents.
