← Files MollieARCHIVED FILE

skills/mollie-payments/references/payments/components.md

6.75 KB · Oct 6, 2026 · 00:02 UTC

↓ Download file

# Mollie Components (Mollie.js)

Mollie Components lets you embed PCI-DSS SAQ-A compliant card fields directly in your checkout.
Card data is captured inside iframes — your server never handles raw card numbers. Instead, the
library exchanges card data for a short-lived `cardToken` that you pass to the Mollie Payments API.

## Full integration flow

1. Load `mollie.js` in the browser
2. Initialise `Mollie(profileId)` with your **profile ID** (not your API key)
3. Create and mount component(s) into your form
4. Listen for `change` events on each component to show validation errors inline
5. On form submit, call `mollie.createToken()` to get a `cardToken`
6. POST the `cardToken` to your backend (hidden form field or JSON body)
7. Backend creates a payment with `method: 'creditcard'` and `cardToken`
8. Redirect the customer to `_links.checkout` for 3D Secure

## 1. Load the script

```html
<!-- Add before </body> — never load from a CDN mirror, only from js.mollie.com -->
<script src="https://js.mollie.com/v1/mollie.js"></script>
```

**CSP headers required:**
```
script-src: js.mollie.com
frame-src:  js.mollie.com
style-src:  'unsafe-inline'
```

## 2. Initialise

```javascript
// profileId is pfl_xxx — find it in the Mollie Dashboard under Developers > Your profile
// Never use your API key (live_xxx / test_xxx) here
const mollie = Mollie('pfl_3RkSN1zuPE', {
  locale: 'en_US',  // controls error message language
  testmode: false,  // set true for test payments; must match the API key used on the backend
});
```

## 3a. Single card component (simplest)

Use this when you want Mollie to render and lay out all fields together.

```html
<form id="checkout-form">
  <div id="card"></div>
  <div id="card-error"></div>
  <button type="submit">Pay</button>
</form>
```

```javascript
const cardComponent = mollie.createComponent('card');
cardComponent.mount('#card');

cardComponent.addEventListener('change', (event) => {
  document.querySelector('#card-error').textContent =
    event.error && event.touched ? event.error : '';
});
```

## 3b. Separate fields (more control over layout)

Use when you need full control over the form layout and styling.

```html
<form id="checkout-form">
  <div>
    <label>Card number</label>
    <div id="card-number"></div>
    <span id="card-number-error" class="field-error"></span>
  </div>
  <div>
    <label>Card holder</label>
    <div id="card-holder"></div>
    <span id="card-holder-error" class="field-error"></span>
  </div>
  <div>
    <label>Expiry date</label>
    <div id="expiry-date"></div>
    <span id="expiry-date-error" class="field-error"></span>
  </div>
  <div>
    <label>CVC / CVV</label>
    <div id="verification-code"></div>
    <span id="verification-code-error" class="field-error"></span>
  </div>
  <button type="submit">Pay</button>
</form>
```

```javascript
const components = {
  cardNumber:       mollie.createComponent('cardNumber'),
  cardHolder:       mollie.createComponent('cardHolder'),
  expiryDate:       mollie.createComponent('expiryDate'),
  verificationCode: mollie.createComponent('verificationCode'),
};

// Mount each component into the corresponding DOM element
components.cardNumber.mount('#card-number');
components.cardHolder.mount('#card-holder');
components.expiryDate.mount('#expiry-date');
components.verificationCode.mount('#verification-code');

// Wire up inline validation errors for each field
for (const [key, component] of Object.entries(components)) {
  const errorEl = document.querySelector(`#${key.replace(/([A-Z])/g, '-$1').toLowerCase()}-error`);
  component.addEventListener('change', (event) => {
    errorEl.textContent = event.error && event.touched ? event.error : '';
  });
}
```

## 4. Tokenise on submit and send to backend

```javascript
document.querySelector('#checkout-form').addEventListener('submit', async (e) => {
  e.preventDefault();

  const { token, error } = await mollie.createToken();

  if (error) {
    // Show the error to the user — do not submit the form
    console.error('Token error:', error);
    return;
  }

  // Option A: hidden form field
  const input = document.createElement('input');
  input.type = 'hidden';
  input.name = 'cardToken';
  input.value = token;
  e.target.appendChild(input);
  e.target.submit();

  // Option B: fetch / XHR (for SPAs)
  // await fetch('/api/create-payment', {
  //   method: 'POST',
  //   body: JSON.stringify({ cardToken: token }),
  //   headers: { 'Content-Type': 'application/json' },
  // });
});
```

**Token validity:** 1 hour. Never cache or reuse tokens.

## 5. Backend: create the payment

```typescript
// Node.js — mollie-api-typescript
import { Client } from 'mollie-api-typescript';

// Injected from your secret manager / deployment config
declare const mollieApiKey: string;

const mollie = new Client({ security: { apiKey: mollieApiKey } });

const payment = await mollie.payments.create({
  paymentRequest: {
    method: 'creditcard',
    amount: { currency: 'EUR', value: '29.99' },
    description: 'Order #1234',
    redirectUrl: 'https://example.com/order/1234/complete',
    webhookUrl: 'https://example.com/webhooks/mollie',
    cardToken: req.body.cardToken,   // ← from the frontend
  },
});

// Redirect the customer — use 303 See Other (GET redirect), never 302/POST
res.redirect(303, payment._links.checkout.href);
```

## 6. Styling

Components render inside iframes, so **text/font styles must be passed via options**, not CSS.
Container-level styles (borders, backgrounds) can be set via CSS classes.

```javascript
const options = {
  styles: {
    base: {
      color: '#1a1a1a',
      fontSize: '16px',
      fontFamily: 'Inter, sans-serif',
      '::placeholder': {
        color: '#9ca3af',
      },
    },
    valid: {
      color: '#059669',
    },
    invalid: {
      color: '#dc2626',
    },
  },
};

const cardNumber = mollie.createComponent('cardNumber', options);
```

CSS classes on the container div:

| Class | When present |
|---|---|
| `.mollie-component` | Always |
| `.mollie-component--is-loading` | While iframe loads |
| `.is-touched` | After first focus |
| `.is-valid` | Valid input |
| `.is-invalid` | Invalid input while focused |

## Common mistakes

| Mistake | Fix |
|---|---|
| Using `live_xxx` / `test_xxx` API key in `Mollie()` | Use `pfl_xxx` profile ID in the browser only |
| Frontend `testmode: false` but test API key on backend | Must both be test or both be live |
| Not redirecting to `_links.checkout` | Always redirect for 3D Secure, even if not required |
| Retrying a failed payment | Create a new token → new payment on failure |
| Styling text via CSS | Pass font/color inside `createComponent(type, { styles })` |
| Token cached between page loads | Tokens expire after 1 hour — generate fresh on each submit |
| Profile ID from wrong account | `pfl_xxx` must belong to the same account as the backend API key |

SHA-256: 34a01dad13afa7bed9cb9219d28c64811ed64418d3d36caab0ca0c8e5c7010c6