> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gnosispay.com/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.


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