---
updatedAt: 2026-08-06T21:00:55.000Z
agentTools:
  projectIndex: https://docs.preczn.com/llms.txt
---

# PaymentElements Integration

Collect card and ACH credentials in Preczn-hosted iframes and exchange them for tokens, without sensitive data ever touching your web app.

## Overview

Preczn PaymentElements is a client-side browser SDK that renders card and ACH inputs inside Preczn-origin iframes. Sensitive account data is captured, validated, and tokenized entirely within Preczn's infrastructure — your page only receives a token. This significantly reduces your PCI scope and qualifies eligible integrators for **SAQ A**.

PaymentElements is the recommended successor to PaymentFields when stronger PCI scope reduction is required. Each input field is hosted in its own iframe served by Preczn, and the SDK coordinates them on your behalf so you can style, observe, and tokenize them with a small JavaScript surface.

> :information\_source: **PaymentElements vs. PaymentFields**
>
> Use **PaymentFields** for a lightweight, drop-in script that intercepts your existing inputs. Use **PaymentElements** when you need iframed inputs for SAQ A eligibility or stronger isolation between your page and cardholder data.

## Implementation Steps

### Step 1: Include the Script

Add the secure JavaScript reference to your page's `<head>` with your **public** API key:

```html
<script src="https://api.preczn.com/v1/clients/preczn.min.js?resources=paymentelements&publicApiKey=2482re32338cd9z5048v7bhmb5"></script>
```

The script auto-bootstraps on `DOMContentLoaded` and exposes its API as `window.Preczn.PaymentElements`.

### Step 2: Define Container Elements

Instead of `<input>` elements, PaymentElements mounts iframes into **container&#x20;**`<div>`**&#x20;elements** that you mark with `data-preczn` attributes. The SDK auto-discovers them on load and watches the DOM for containers added later.

**Card form:**

```html
<div data-preczn="number"     data-preczn-label="Card number"></div>
<div data-preczn="expiration" data-preczn-label="Expiration (MM/YY)"></div>
<div data-preczn="cvv"        data-preczn-label="CVV"></div>
```

**ACH form:**

```html
<div data-preczn="accountNumber" data-preczn-label="Account number"></div>
<div data-preczn="routingNumber" data-preczn-label="Routing number"></div>
<div data-preczn="accountType"   data-preczn-label="Account type"></div>
<div data-preczn="bankCountry"   data-preczn-label="Bank country"></div>
```

Supported `data-preczn` values:

| Value           | Element rendered  | Purpose                     |
| --------------- | ----------------- | --------------------------- |
| `number`        | Card number input | Card PAN entry              |
| `expiration`    | Expiration input  | MM/YY entry                 |
| `cvv`           | CVV input         | 3–4 digit security code     |
| `accountNumber` | ACH input         | Bank account number         |
| `routingNumber` | ACH input         | Bank routing number         |
| `accountType`   | Native picker     | `checking`, `savings`, etc. |
| `bankCountry`   | Native picker     | `US`, `CA`                  |

> :warning: **Do not put&#x20;**`id`**&#x20;or&#x20;**`name`**&#x20;attributes on these containers, and do not nest them inside a&#x20;**`<form>`**&#x20;element.** PaymentElements iframes never expose values to the parent page; placing them in a form can cause browsers to intercept submission attempts.

The container's width and height determine the iframe's size — apply your own layout, padding, and border styles to the container, and use `configure()` (Step 3) to style the input *inside* the iframe.

### Step 3: Configure Styles and Placeholders

Call `configure()` to style the inputs inside the iframes and set placeholder text. Styles are applied across all mounted fields and can be updated at any time.

```javascript
Preczn.PaymentElements.configure({
  style: {
    base: {
      color: '#1a1a1a',
      fontFamily: 'system-ui, -apple-system, sans-serif',
      fontSize: '16px',
      fontWeight: '400',
      lineHeight: '24px',
      '::placeholder': {
        color: '#9ca3af'
      }
    },
    focus:    { color: '#0f172a' },
    invalid:  { color: '#dc2626' },
    complete: { color: '#16a34a' }
  },
  placeholders: {
    number:        '1234 5678 9012 3456',
    expiration:    'MM / YY',
    cvv:           'CVC'
  }
});
```

**Pseudo-states:** `base`, `focus`, `invalid`, `complete`, `empty`. The `::placeholder` selector accepts `color` and `fontStyle`.

**Allowed style properties:** `color`, `fontFamily`, `fontSize`, `fontWeight`, `fontStyle`, `fontSmoothing`, `lineHeight`, `letterSpacing`, `textAlign`, `padding`, `textTransform`.

**Constraints (enforced for security):**

* `fontFamily` must be drawn from: `system-ui`, `-apple-system`, `Segoe UI`, `Roboto`, `Helvetica`, `Arial`, `sans-serif`, `serif`, `monospace`.
* `fontSize` must be in the range `8–48px` or `0.5–3rem`.
* Properties not on the allowlist (e.g., `background`, `marginTop`, `url(...)`) are silently dropped.

> :information\_source: Calling `configure()` before any container is mounted is fine — the configuration is applied to each field as it mounts. You can call `configure()` again later to update styles or placeholders live.

### Step 4: Listen for Field Events (optional)

Subscribe to per-field events to drive UI affordances (e.g., a brand icon, a "valid" checkmark, an error message under a field):

```javascript
Preczn.PaymentElements.on('number', 'change', function (state) {
  // state = { empty, complete, brand, error }
  if (state.brand) {
    document.getElementById('brand-icon').className = 'brand-' + state.brand.toLowerCase();
  }
});

Preczn.PaymentElements.on('cvv', 'change', function (state) {
  document.getElementById('cvv-error').textContent = state.error || '';
});
```

Available events:

| Event    | Payload                               | Fires when                             |
| -------- | ------------------------------------- | -------------------------------------- |
| `ready`  | `{}`                                  | Field iframe has mounted and is usable |
| `change` | `{ empty, complete, brand?, error? }` | User input changes the field state     |
| `focus`  | `{}`                                  | Field gains focus                      |
| `blur`   | `{}`                                  | Field loses focus                      |

Remove a handler with `Preczn.PaymentElements.off(field, event, handler)`.

If you need to wait until every field is mounted before enabling a submit button:

```javascript
Preczn.PaymentElements.ready().then(function () {
  document.getElementById('submit').disabled = false;
});
```

### Step 5: Obtain a Token

When the customer submits the form, request a token. PaymentElements collects values from the iframes, validates them, posts them to Preczn's tokenization endpoint, and invokes your callback with the result.

**Define a callback function:**

```javascript
var tokenCallback = function (result, errors) {
  if (errors) {
    console.error('Tokenization failed', errors);
    return;
  }
  var precznToken = result.token;
  // Send precznToken to your server to charge or store
};
```

**Card token:**

```javascript
Preczn.PaymentElements.getSingleUseToken(tokenCallback);
```

**ACH token:**

```javascript
Preczn.PaymentElements.getSingleUseAchToken(tokenCallback);
```

## Utility Functions

| Function              | Returns                                                                                                                                          |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `getCardBrand()`      | Card brand string once a card number has been entered, otherwise `null`. Possible values: `VISA`, `MASTERCARD`, `AMEX`, `DISCOVER/JCB`, `OTHER`. |
| `isValidCardNumber()` | `true` once the entered card number passes Luhn and length checks; `false` if entered but invalid; `null` if no input has been received yet.     |
| `ready()`             | A `Promise<void>` that resolves once every registered field has mounted.                                                                         |

```javascript
if (Preczn.PaymentElements.isValidCardNumber()) {
  console.log('Brand:', Preczn.PaymentElements.getCardBrand());
}
```

## Security Notes

* Always use **public** API keys (`priv_test…` / `priv_live_…`); never use private/secret keys in browser code.
* Sensitive values (PAN, CVV, account/routing numbers) never enter your page's JavaScript context, DOM, storage, or postMessage channels — they only exist inside Preczn-served iframes.
* The SDK coordinates with iframes via authenticated cross-frame channels and requires that messages originate from the SDK it boots; spoofed `postMessage` traffic from other origins is ignored.
* Your page's **Content Security Policy** must allow:
  * `script-src https://api.preczn.com`
  * `frame-src https://api.preczn.com`
  * `connect-src https://api.preczn.com`
* Do not use `id`, `name`, or wrap PaymentElements containers in a `<form>`. Mixing PaymentElements containers with PaymentFields-style inputs in the same form is unsupported and will log a console warning.
* For a full PCI Shared Responsibility Matrix and SAQ A attestation package, contact your Preczn account team.