> ## Documentation Index
> Fetch the complete documentation index at: https://gnosispay-feat-v2-docs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# PSE Integration

> Securely display and manage card data using the Gnosis Pay Payment Secure Elements (PSE) SDK

<Note>
  New to authentication on Gnosis Pay? Read [Authentication & Tokens](/concepts/auth/token) first the PSE SDK relies on a valid access token (`authModuleToken`) obtained via the [SIWE authentication flow](/guides/auth-with-siwe).
</Note>

The **Payment Secure Elements (PSE) SDK** lets you display and manage sensitive card data such as card number, expiration, CVV, and PIN directly in your application, without that data ever touching your own front-end or back-end code. Card data is rendered inside **PCI-compliant iframes** hosted by Gnosis Pay, and communicates with your app only through secure, origin-verified `postMessage` events.

<Info>
  PSE v3 requires a wallet signature (EIP-712) before each sensitive operation, adding a second factor on top of your `authModuleToken`. This is the current supported version.
</Info>

***

## Installation

Install via npm:

```bash theme={null}
npm install @gnosispay/pse-sdk
```

Or load directly via CDN:

```html theme={null}
<!-- For modern browsers (ES modules) -->
<script
  type="module"
  src="https://unpkg.com/@gnosispay/pse-sdk@2.0.0/dist/gp-sdk.es.js"
></script>

<!-- For older browsers (UMD) -->
<script src="https://unpkg.com/@gnosispay/pse-sdk@2.0.0/dist/gp-sdk.umd.js"></script>
```

***

## Backend Setup: mTLS and the Ephemeral Token

Before initializing the SDK on the front-end, your **backend** must establish a secure connection to the PSE private API to obtain an **ephemeral token**.

### Secure connection using mTLS authentication

**Mutual TLS (mTLS)** is a form of authentication where both parties in a connection verify each other using the TLS protocol. Your backend establishes an mTLS connection with the Gnosis Pay private PSE API to receive an ephemeral token.

#### Generating mTLS certificates

After signing up through the [Partners Dashboard](https://partners.gnosispay.com/), you'll receive an `App ID` instantly. Use it to generate a private key and Certificate Signing Request (CSR):

```bash theme={null}
# APP_ID is a string starting with `gp_` that you have received from Gnosis Pay
export APP_ID="gp_woop_123"

# Create a private key (NEVER share with anyone)
openssl ecparam -name prime256v1 -genkey -noout -out "${APP_ID}.key.pem"

# Create the CSR (OK to share)
openssl req -new -sha256 -key "${APP_ID}.key.pem" -out "${APP_ID}.csr.pem" -subj "/CN=${APP_ID}"
```

<Warning>
  Share only `${APP_ID}.csr.pem` with the Gnosis Pay team. **Never** share the `.key.pem` private key file with anyone.
</Warning>

Once we receive your CSR, we'll sign it and return your signed certificates. These, along with your private key, are used to establish the mTLS connection.

#### Establishing mTLS authentication (Node.js)

Store your signed certificates and private key securely in your environment:

```bash theme={null}
SIGNED_CERTIFICATES="-----BEGIN CERTIFICATE-----
ABCQz ....
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
DEFC7 ....
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
GHICc ....
-----END CERTIFICATE-----"

PRIVATE_KEY="-----BEGIN EC PRIVATE KEY-----
ABCD....
-----END EC PRIVATE KEY-----"
```

<CodeGroup>
  ```js Using Axios theme={null}
  const httpsAgent = new https.Agent({
    cert: process.env.SIGNED_CERTIFICATES,
    key: process.env.PRIVATE_KEY,
    rejectUnauthorized: true, // Ensure SSL verification
  });

  const ephemeralTokenRequest = await axios({
    httpsAgent: httpsAgent,
    method: "POST",
    url: `https://api-pse.gnosispay.com/api/v1/ephemeral-token`,
    headers: { "Content-Type": "application/json" },
  });
  ```

  ```js Using https.request theme={null}
  import https from "https";

  const httpsAgent = new https.Agent({
    cert: CERT,
    key: KEY,
    rejectUnauthorized: true,
  });

  const req = https.request(
    {
      hostname: "api-pse.gnosispay.com",
      path: "/api/v1/ephemeral-token",
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "User-Agent": "User-Client/1.0.0",
      },
      agent: httpsAgent,
    },
    (res) => {
      let data = "";
      res.on("data", (chunk) => {
        data += chunk;
      });

      res.on("end", () => {
        console.log("Status:", res.statusCode);
        try {
          const parsedData = JSON.parse(data);
          console.log("Response:", parsedData);
        } catch {
          console.log("Raw response:", data);
        }
      });
    }
  );

  req.on("error", (error) => {
    console.error("Request error:", error);
  });
  req.end();
  ```
</CodeGroup>

<Note>
  The ephemeral token is valid for a very short time frame. Generate a new one for every SDK usage.
</Note>

### Backend: ephemeral token relay endpoint

Your backend needs an endpoint that proxies ephemeral token requests to the PSE private API using the mTLS setup above. Example using Express:

```js theme={null}
import express from "express";
import axios from "axios";
import https from "node:https";

const app = express();

app.get("/api/ephemeral-token", async (_req, res) => {
  try {
    const cert = Buffer.from(process.env.CLIENT_CERT, "base64").toString("ascii");
    const key = Buffer.from(process.env.CLIENT_KEY, "base64").toString("ascii");
    const httpsAgent = new https.Agent({ cert, key, rejectUnauthorized: true });

    const response = await axios({
      method: "POST",
      url: "https://api-pse.gnosispay.com/api/v1/ephemeral-token",
      headers: { "Content-Type": "application/json" },
      httpsAgent,
    });

    res.json({ data: response.data.data });
  } catch (error) {
    res.status(502).json({ error: "Failed to reach PSE private API" });
  }
});
```

`CLIENT_CERT` and `CLIENT_KEY` are the base64-encoded signed certificate and private key stored in your environment variables.

***

## EIP-712 Two-Factor Authentication

PSE v3 requires a **wallet signature** before each sensitive operation. This acts as a second factor: even if an `authModuleToken` is compromised, an attacker cannot view card data or change a PIN without also controlling the user's wallet.

### How it works

Before initializing the SDK, your front-end must:

1. Request a one-time **challenge** from the auth module.
2. Ask the user's wallet to **sign** the returned EIP-712 typed data.
3. Pass the resulting **signature** and **nonce** to the SDK constructor.

The PSE service forwards both values to the auth module, which verifies the signature and marks the nonce as consumed. Each challenge is **single-use** and **expires after 5 minutes**.

### Get PCI EIP-712 Challenge

Issues a one-time EIP-712 challenge. The response is a complete typed-data object that can be passed directly to `signTypedData` (convert `message.nonce` to `BigInt` first).

<Tabs>
  <Tab title="Sandbox">
    ```bash theme={null}
        curl --request GET \
          --url "https://core.sandbox.gnosispay.in/user-api/pci/cards/{cardId}/challenge?action=view-details" \
          --header 'Authorization: Bearer <authModuleToken>'
    ```
  </Tab>

  <Tab title="Production">
    ```bash theme={null}
        curl --request GET \
          --url "https://core.prod.gnosispay.com/user-api/pci/cards/{cardId}/challenge?action=view-details" \
          --header 'Authorization: Bearer <authModuleToken>'
    ```
  </Tab>
</Tabs>

<Note>
  `message.nonce` is returned as a decimal string representing a `uint256`. Convert it to `BigInt` before passing it to `signTypedData`.
</Note>

### Signing the challenge

<CodeGroup>
  ```typescript Using wagmi theme={null}
  import { useSignTypedData } from "wagmi";

  const { signTypedDataAsync } = useSignTypedData();

  // 1. Fetch the challenge (authModuleToken is sent automatically via client config)
  const { data, error } = await getPciCardsByCardIdChallenge({
    path: { cardId },
    query: { action: "view-details" },
  });

  if (error || !data) throw new Error("Failed to get EIP-712 challenge");

  // 2. Sign and convert nonce to BigInt for the uint256 type
  const signature = await signTypedDataAsync({
    domain: data.domain,
    types: data.types,
    primaryType: data.primaryType,
    message: {
      authorization: data.message.authorization,
      nonce: BigInt(data.message.nonce),
    },
  });

  // 3. Pass to SDK constructor
  const eip712Signature = signature;
  const eip712Nonce = data.message.nonce; // keep as decimal string
  ```

  ```typescript Using viem directly theme={null}
  import { createWalletClient, custom } from "viem";
  import { celo } from "viem/chains";

  const walletClient = createWalletClient({
    chain: celo,
    transport: custom(window.ethereum),
  });

  // 1. Fetch the challenge
  const response = await fetch(
    `https://core.sandbox.gnosispay.in/user-api/pci/cards/${cardId}/challenge?action=view-details`,
    { headers: { Authorization: `Bearer ${authModuleToken}` } }
  );
  const data = await response.json();

  // 2. Sign
  const [account] = await walletClient.getAddresses();
  const signature = await walletClient.signTypedData({
    account,
    domain: data.domain,
    types: data.types,
    primaryType: data.primaryType,
    message: {
      authorization: data.message.authorization,
      nonce: BigInt(data.message.nonce),
    },
  });

  const eip712Signature = signature;
  const eip712Nonce = data.message.nonce;
  ```
</CodeGroup>

<Warning>
  The challenge must be fetched **immediately before** each SDK initialization. Do not reuse a nonce across different operations or SDK instances as it will be rejected after the first use.
</Warning>

***

## Initialize the SDK

After completing the EIP-712 challenge/sign step above, initialize the SDK with `pseVersion: 3`:

```typescript theme={null}
import GPSDK, { ElementType } from "@gnosispay/pse-sdk";

// 1. Get the auth module access token from your SIWE auth flow
const authModuleToken = getAccessToken(); // your auth implementation

// 2. Fetch ephemeral token from your backend
const response = await fetch("/api/ephemeral-token");
const { data } = await response.json();

// 3. Fetch and sign an EIP-712 challenge (see section above)
//    Use the action matching the operation you are about to perform:
//    "view-details" | "view-pin" | "change-pin"
const { eip712Signature, eip712Nonce } = await getSignedChallenge(cardId, "view-details");

// 4. Initialize the SDK with PSE version 3
const gpSdk = new GPSDK({
  pseVersion: 3,                          // PSE version 3, required for EIP-712 2FA
  appId: "gp_your_app_id",               // Your Partner App ID
  ephemeralToken: data.ephemeralToken,
  authModuleToken: authModuleToken,       // Auth module access token
  eip712Signature: eip712Signature,       // Hex signature from wallet
  eip712Nonce: eip712Nonce,               // Decimal nonce string from challenge
  onActionSuccess: (action) => {
    console.log("Action completed:", action);
  },
  onInvalidToken: (message) => {
    console.error("Token invalid:", message);
    // Refresh the ephemeral token and reinitialize
  },
  onError: (message, details) => {
    console.error("PSE error:", message, details);
  },
});
```

<Warning>
  The `authModuleToken` expires every 15 minutes. Handle the `onInvalidToken` callback to refresh your access token via the [Authenticating with SIWE](guides/auth-with-siwe#refresh-access-token) flow and re-initialize the SDK.
</Warning>

### Display card details

Use `ElementType.CardData` to display the full card number, expiration date, and security code.

<Note>
  Use `action: "view-details"` when fetching the EIP-712 challenge for this element.
</Note>

```typescript theme={null}
// cardId is a UUID obtained from GET /cards
const { destroy } = gpSdk.init(ElementType.CardData, "#card-data-container", {
  cardId: "019c9578-a8ce-7445-9961-51b945605f70",
});

// Call destroy() when unmounting or cleaning up
destroy();
```

### View card PIN

Use `ElementType.CardPin` to display the card's current PIN inside the secure iframe.

<Note>
  Use `action: "view-pin"` when fetching the EIP-712 challenge for this element.
</Note>

```typescript theme={null}
// Fetch and sign the challenge with action "view-pin" before constructing the SDK
const { eip712Signature, eip712Nonce } = await getSignedChallenge(cardId, "view-pin");

const gpSdk = new GPSDK({
  pseVersion: 3,
  appId: "gp_your_app_id",
  ephemeralToken: data.ephemeralToken,
  authModuleToken: authModuleToken,
  eip712Signature,
  eip712Nonce,
  onActionSuccess: (action) => { /* ... */ },
  onInvalidToken: (message) => { /* ... */ },
  onError: (message, details) => { /* ... */ },
});

const { destroy } = gpSdk.init(ElementType.CardPin, "#pin-container", {
  cardId: "019c9578-a8ce-7445-9961-51b945605f70",
});
```

### Set / change card PIN

Use `ElementType.SetCardPin` to render a PIN entry form that lets the cardholder set or change their PIN.

<Note>
  Use `action: "change-pin"` when fetching the EIP-712 challenge for this element.
</Note>

```typescript theme={null}
// Fetch and sign the challenge with action "change-pin" before constructing the SDK
const { eip712Signature, eip712Nonce } = await getSignedChallenge(cardId, "change-pin");

const gpSdk = new GPSDK({
  pseVersion: 3,
  appId: "gp_your_app_id",
  ephemeralToken: data.ephemeralToken,
  authModuleToken: authModuleToken,
  eip712Signature,
  eip712Nonce,
  onActionSuccess: (action) => {
    // Fired with Action.SetPin when the user submits the new PIN,
    // and Action.DoneSettingPin when the operation is confirmed.
    console.log("PIN operation completed:", action);
  },
  onInvalidToken: (message) => { /* ... */ },
  onError: (message, details) => { /* ... */ },
});

const { destroy } = gpSdk.init(ElementType.SetCardPin, "#set-pin-container", {
  cardId: "019c9578-a8ce-7445-9961-51b945605f70",
});
```

### Refresh the ephemeral token

If the current ephemeral token has expired, you'll receive an `onInvalidToken` callback. Refresh it without re-creating the SDK instance:

```typescript theme={null}
const newToken = await fetchNewEphemeralToken();
gpSdk.refreshToken(newToken);
```

***

## Available Elements

| Element                              | Description                                      |
| ------------------------------------ | ------------------------------------------------ |
| `ElementType.CardData`               | Read credit card number, expiration date and CVV |
| `ElementType.CardPin`                | Read PIN of credit card                          |
| `ElementType.SetCardPin`             | Set PIN of credit card                           |
| `ElementType.CardExpirationCopied`   | User copied card expiration date                 |
| `ElementType.CardSecurityCodeCopied` | User copied card security code                   |
| `ElementType.CardNumberCopied`       | User copied card number                          |

Each element is rendered in a secure iframe to ensure PCI compliance.

## Element Lifecycle

Elements can be initialized and destroyed:

```js theme={null}
const cardDataContainer = gpSdk.init(
  ElementType.CardData,
  "#card-data-container",
  {
    cardId: "your-card-id",
  }
);

// When you're done with the element
cardDataContainer.destroy();
```

## Callbacks

The SDK provides callbacks to handle events from the iframe elements:

| Callback                    | Trigger                                                                        |
| --------------------------- | ------------------------------------------------------------------------------ |
| `onActionSuccess(action)`   | A user action completes (e.g., `CardNumberCopied`, `SetPin`, `DoneSettingPin`) |
| `onInvalidToken(message)`   | The ephemeral token has expired or is invalid                                  |
| `onError(message, details)` | An error occurred inside the iframe                                            |

***

## Customizing Element Styling

For security reasons, the only way to apply custom styling to iframe elements is to prepare and share a **CSS file** with the Gnosis Pay team. This file, named `<partner_name>.css`, will be incorporated into the iframe.

Standard styling is applied to iframe elements by default. Selectors you can override include:

* `.pse-container` : shared class for all iframe containers
* `#pse-card-data-container` : main container for displaying card data
* `.pse-card-field` : container for each card data field (card number, expiry date, security code)
* `.pse-card-label` : labels for each field
* `.pse-card-value` : container for the actual card data values

### Styling workflow

1. In your front-end, load the element you wish to customize (e.g., the card data).
2. Locate the custom CSS file with your name in either the "**Style Editor**" in Firefox or the "**Sources**" panel in Chrome/Brave (e.g., `gnosis_pay_ui.css`).
3. Apply your desired styling changes reflect immediately in your interface.
4. Save the file and send it to Gnosis Pay for application in production.
