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

# Embedded widget

> Display card PAN, CVV, and expiry via a Contro-hosted iframe widget

## Overview

Partners who are **not PCI DSS compliant** can display sensitive card data to end-users using a Contro-hosted HTML widget embedded in an iframe. Sensitive data never reaches your servers - reducing PCI scope to **SAQ-A**.

<Info>
  If your organization is PCI DSS compliant and you prefer full UI control, use the [direct API](/partner/direct-api-reveal) method instead.
</Info>

## Prerequisites

1. A Contro partner account with API keys
2. Configure your **allowed origins** in the partner dashboard [https://partner.contro.me/settings](https://partner.contro.me/settings) <Icon icon="arrow-up-right-from-square" size={12} /> - this controls which domains can embed the iframe

## Step 1: Generate a reveal URL

Call the reveal-html endpoint to get a short-lived, single-use signed URL:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.contro.me/v1/partner/cards/{card_id}/reveal-html \
    -H "x-contro-api-key: $CONTRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "stylesheetUrl": "https://cdn.example.com/card-widget.css",
      "copyPan": true
    }'
  ```

  ```typescript SDK (Node) theme={null}
  const { accessUrl } = await contro.cards.revealHtml("card_def456", {
    stylesheetUrl: "https://cdn.example.com/card-widget.css",
    copyPan: true,
  });
  ```

  ```python SDK (Python) theme={null}
  result = contro.cards.reveal_html("card_def456",
      stylesheet_url="https://cdn.example.com/card-widget.css",
      copy_pan=True,
  )
  access_url = result.access_url
  ```
</CodeGroup>

**Response:**

```json theme={null}
{
  "accessUrl": "https://api.contro.me/v1/web/partner-card-details?token=eyJ..."
}
```

<Warning>
  The URL expires after **60 seconds** and can only be used **once**. Generate a fresh URL each time the user opens the card reveal UI.
</Warning>

## Step 2: Embed the iframe

```html theme={null}
<iframe
  src="ACCESS_URL_FROM_STEP_1"
  width="400"
  height="300"
  frameborder="0"
  allow="clipboard-write"
></iframe>
```

The `clipboard-write` permission allows users to copy card fields to their clipboard.

## Step 3: Custom styling (optional)

Pass a `stylesheetUrl` in the request body to apply custom CSS. The stylesheet must be served over HTTPS.

The widget exposes these DOM IDs for styling:

| ID | Element |
| - | - |
| `#card-data` | Main container |
| `#pan-div` | Card number field container |
| `#expiry-div` | Expiry date field container |
| `#cvv-div` | CVV field container |
| `#pan-value` | Card number value |
| `#expiry-value` | Expiry date value |
| `#cvv-value` | CVV value |

## Sandbox testing

Use `sk_test_` API keys to generate sandbox URLs. The widget displays test data with a visible **SANDBOX** badge:

| Field | Value |
| - | - |
| Card Number | `4000 0000 0000 0000` |
| CVV | `123` |
| Expiry | `12/2030` |
| Name | `TEST CARDHOLDER` |

## Security

* **CSP headers**: The widget sets strict `Content-Security-Policy` headers including `frame-ancestors` restricted to your configured allowed origins
* **Allowed origins (iframe)**: Configure via dashboard **Settings**. These control which domains can embed the card widget iframe — separate from the [API allowed origins](/partner/authentication#allowed-origins) that enforce CORS. In production, unconfigured origins result in `frame-ancestors 'none'` (iframe blocked)
* **Single-use tokens**: Each URL can only be loaded once - replay attacks are rejected
* **60-second expiry**: Tokens expire quickly to minimize the attack window
