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

# Setting a Card PIN

> Let cardholders set or change their PIN through Yativo's hosted secure page — works for both virtual and physical cards

Every card — virtual and physical — starts with `pin_set: false`. **Cardholders must set their PIN before card details (card number, CVV) can be viewed, and before chip-and-PIN or ATM transactions will work.**

To let a cardholder set their PIN, call the view-token endpoint with `enabled_views: ["pin-set"]`. The API always returns **200 OK** with a `secure_view_url` for PIN setup — regardless of whether `pin_set` is `true` or `false`. Open that URL in an iframe or WebView and the cardholder can set (or change) their PIN directly.

<Note>
  The `422 PIN_NOT_SET` error only occurs when you request **card data or PIN reveal** (`enabled_views: ["data"]`, `["pin-view"]`) while `pin_set` is still `false`. It does **not** occur when requesting `["pin-set"]` to set the PIN.
</Note>

Card PINs are set and changed through Yativo's **hosted secure page**, powered by Gnosis Pay's Partner Secure Elements (PSE). The cardholder types their PIN directly inside a sandboxed iframe hosted by Yativo. The PIN never passes through your backend.

***

## How it works

<Steps>
  <Step title="Your backend requests a PIN view token">
    Call `POST /v1/yativo-card/customers/{yativoCardId}/cards/{cardId}/view-token` with `enabled_views: ["pin-set"]` for initial setup or PIN change, or `["pin-view"]` to reveal the current PIN. Pass your brand `theme` to customise the page.
  </Step>

  <Step title="You receive a secure_view_url">
    The API returns **200 OK** with a short-lived `secure_view_url` and the current `pin_set` status. This always succeeds for `["pin-set"]` requests — even when `pin_set` is `false`.
  </Step>

  <Step title="Show the hosted page to the cardholder">
    Open the URL in an `<iframe>`, mobile WebView, or a new browser tab in your app. The cardholder enters and confirms their 4-digit PIN inside the hosted page.
  </Step>

  <Step title="Gnosis Pay confirms via webhook">
    Once saved, Gnosis Pay fires `physical.card.pin.changed`. Yativo receives it and flips `pin_set` to `true` on the card record.
  </Step>
</Steps>

The same flow handles both **initial PIN setup** and **PIN changes** — use `enabled_views: ["pin-set"]` in both cases.

***

## Request a PIN view token

Call this from your **backend** using your issuer access token.

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

<ParamField path="yativoCardId" type="string" required>
  The customer's `yativo_card_id`.
</ParamField>

<ParamField path="cardId" type="string" required>
  The card ID — works for both virtual and physical cards.
</ParamField>

<ParamField body="enabled_views" type="array" required>
  Controls which tabs appear on the hosted page. Accepted values:

  | Value        | Tab shown                                           |
  | ------------ | --------------------------------------------------- |
  | `"pin-set"`  | Set or change PIN                                   |
  | `"pin-view"` | Reveal current PIN (requires `pin_set: true`)       |
  | `"data"`     | Card number, CVV, expiry (requires `pin_set: true`) |

  Pass `["pin-set"]` for initial setup or PIN change. Pass `["data", "pin-view", "pin-set"]` to show all three tabs at once.
  The legacy value `"pin"` is accepted as an alias for `"pin-set"`.
</ParamField>

<ParamField body="theme" type="object">
  Brand the hosted page with your colours and logo. All fields optional.

  | Field              | Type   | Description                           |
  | ------------------ | ------ | ------------------------------------- |
  | `accent_color`     | string | Hex colour for buttons and highlights |
  | `background_color` | string | Page background                       |
  | `panel_color`      | string | Card panel background                 |
  | `text_color`       | string | Primary text                          |
  | `logo_url`         | string | Your logo at the top of the page      |
  | `border_radius`    | number | Corner radius in px (8–36)            |
  | `font_family`      | string | CSS font-family string                |
</ParamField>

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST 'https://crypto-api.yativo.com/api/v1/yativo-card/customers/yativo_card_customer_8f9a..._1769031332068/cards/afeb85fe-02f8-48da-b61e-84ad02704167/view-token' \
    -H 'Authorization: Bearer YOUR_ISSUER_ACCESS_TOKEN' \
    -H 'Content-Type: application/json' \
    -d '{
      "enabled_views": ["pin-set"],
      "theme": {
        "accent_color": "#6366f1",
        "background_color": "#f5f5f5",
        "logo_url": "https://yourapp.com/logo.png",
        "border_radius": 16
      }
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch(
    `https://crypto-api.yativo.com/api/v1/yativo-card/customers/${yativoCardId}/cards/${cardId}/view-token`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${issuerAccessToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        enabled_views: ['pin-set'],
        theme: {
          accent_color: '#6366f1',
          logo_url: 'https://yourapp.com/logo.png',
        },
      }),
    }
  );

  const { data } = await response.json();
  // Pass data.secure_view_url to your frontend
  ```
</RequestExample>

<ResponseExample>
  ```json 200 OK — PIN not yet set theme={null}
  {
    "success": true,
    "data": {
      "secure_view_url": "https://crypto-api.yativo.com/api/v1/yativo-card/view/eyJhbGci...",
      "expires_at": "2026-05-18T12:05:00.000Z",
      "last_four": "4291",
      "card_type": "virtual",
      "holder_name": "Jane Doe",
      "pin_set": false,
      "enabled_views": ["pin-set"],
      "requires_access_code": false
    }
  }
  ```

  ```json 200 OK — PIN already set (change PIN) theme={null}
  {
    "success": true,
    "data": {
      "secure_view_url": "https://crypto-api.yativo.com/api/v1/yativo-card/view/eyJhbGci...",
      "expires_at": "2026-05-18T12:05:00.000Z",
      "last_four": "4291",
      "card_type": "virtual",
      "holder_name": "Jane Doe",
      "pin_set": true,
      "enabled_views": ["pin-set"],
      "requires_access_code": false
    }
  }
  ```
</ResponseExample>

***

## Embed the hosted PIN page

Pass `secure_view_url` directly as the `src` of an iframe. No SDK or client-side JavaScript required.

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

<Tip>
  Recommended iframe dimensions: `420 × 520px` for PIN-only. Use `420 × 740px` if showing card details and PIN together.
</Tip>

***

## Checking PIN status

`pin_set` is updated when Yativo receives the `physical.card.pin.changed` webhook from Gnosis Pay. It is **not** sourced from the Gnosis API — treat it as the last known state.

You can read it from:

**Per-card**, in `GET /v1/card-issuer/customers/{customerId}`:

```json theme={null}
{
  "card_id": "afeb85fe-02f8-48da-b61e-84ad02704167",
  "card_type": "virtual",
  "status": "active",
  "pin_set": false
}
```

**Per-customer** top-level (true only when every card has a PIN):

```json theme={null}
{
  "cards_count": 2,
  "pin_set": false,
  "next_action": "Card is active — customer must set card PIN before in-store (PSE) payments will work"
}
```

**On the view-token response** — `data.pin_set` is returned every time you call the view-token endpoint.

To list all customers who still need to set a PIN:

```bash theme={null}
GET /v1/card-issuer/customers?status=card_created
```

Then filter the results by `pin_set: false`.

<Note>
  `pin_set` reflects the last state received via webhook and may lag if a webhook was delayed. If a cardholder reports unexpected `PIN_NOT_SET` errors after completing PIN setup, they can retry — the server will re-evaluate when the webhook catches up.
</Note>

***

## Webhook confirmation

Subscribe to `physical.card.pin.changed` on your webhook endpoint. Yativo updates `pin_set: true` automatically when this event arrives — no polling needed.

```json Payload example theme={null}
{
  "event": "physical.card.pin.changed",
  "data": {
    "cardToken": "afeb85fe-02f8-48da-b61e-84ad02704167",
    "yativo_card_id": "yativo_card_customer_8f9a..._1769031332068"
  }
}
```

***

## Notes

| Detail                        | Value                                                                        |
| ----------------------------- | ---------------------------------------------------------------------------- |
| Works for                     | Virtual and physical cards                                                   |
| PIN format                    | Exactly 4 digits (`0000`–`9999`)                                             |
| Initial setup & change        | Same `enabled_views: ["pin-set"]` flow for both                              |
| PIN never touches your server | Entered directly in the Yativo-hosted iframe                                 |
| Brand customisation           | Pass `theme` in the token request — applies to the hosted PIN page           |
| Confirmation                  | `physical.card.pin.changed` webhook → `pin_set: true`                        |
| `pin_set` source              | Webhook only — not from Gnosis API                                           |
| Online vs chip PIN            | PSE updates the online PIN. The chip syncs on first ATM use (physical cards) |
