Skip to main content
This tutorial was written by Claude Code (an AI) and has not yet been reviewed. Follow along with caution. If the tutorial was helpful or a specific part was not clear/correct, please provide feedback at the bottom of the page. Thank you.
This guide covers integrating Mollie with CoCart Preview API. Requires CoCart v4.6+ and a configured Mollie payment gateway.

Overview

Mollie integration with CoCart uses Mollie Components (Mollie.js) for secure tokenization of payment data. This ensures sensitive card information never touches your server while providing PCI DSS SAQ A compliance and access to 30+ European payment methods. Mollie is the leading payment service provider in Europe, particularly strong in the Netherlands, Belgium, and Germany.

Prerequisites

Before implementing Mollie checkout, ensure you have:
  1. Mollie payment gateway configured in WooCommerce
  2. Mollie.js library loaded in your frontend
  3. A valid cart with items added
  4. Customer billing address information
  5. Mollie profile ID (found in Dashboard > Developers > API keys)

Integration Flow

1

Load Mollie.js

Initialize the Mollie JavaScript library
2

Initialize Mollie Object

Create Mollie instance with your profile ID
3

Create Components

Set up secure iframe-based payment input fields
4

Collect Payment Details

Securely collect card information from customers
5

Generate Card Token

Create a temporary card token using Mollie.js
6

Complete Checkout

Submit checkout with card token to CoCart for processing

Step 1: Load Mollie.js

Include the Mollie.js library in your checkout page:
<!-- Load Mollie.js library -->
<script src="https://js.mollie.com/v1/mollie.js"></script>
Mollie.js is always served over HTTPS and automatically stays up to date with the latest version.

Step 2: HTML Structure

Create a checkout form with containers for Mollie Components:
<form id="checkout-form">
    <!-- Customer Information -->
    <div class="billing-section">
        <h3>Billing Information</h3>
        <input type="text" name="billing_first_name" placeholder="First Name" required>
        <input type="text" name="billing_last_name" placeholder="Last Name" required>
        <input type="email" name="billing_email" placeholder="Email" required>
        <input type="tel" name="billing_phone" placeholder="Phone">
        <input type="text" name="billing_address_1" placeholder="Address" required>
        <input type="text" name="billing_city" placeholder="City" required>
        <input type="text" name="billing_state" placeholder="State" required>
        <input type="text" name="billing_postcode" placeholder="Postal Code" required>
        <select name="billing_country" required>
            <option value="NL">Netherlands</option>
            <option value="BE">Belgium</option>
            <option value="DE">Germany</option>
            <option value="FR">France</option>
            <option value="GB">United Kingdom</option>
            <!-- Add other countries -->
        </select>
    </div>

    <!-- Payment Information -->
    <div class="payment-section">
        <h3>Payment Information</h3>

        <!-- Mollie Components will be mounted here -->
        <div class="mollie-components">
            <div class="form-group">
                <label for="card-holder">Cardholder Name</label>
                <div id="card-holder" class="mollie-component"></div>
                <div id="card-holder-error" class="error-message"></div>
            </div>

            <div class="form-group">
                <label for="card-number">Card Number</label>
                <div id="card-number" class="mollie-component"></div>
                <div id="card-number-error" class="error-message"></div>
            </div>

            <div class="card-row">
                <div class="form-group">
                    <label for="expiry-date">Expiry Date</label>
                    <div id="expiry-date" class="mollie-component"></div>
                    <div id="expiry-date-error" class="error-message"></div>
                </div>

                <div class="form-group">
                    <label for="verification-code">CVC</label>
                    <div id="verification-code" class="mollie-component"></div>
                    <div id="verification-code-error" class="error-message"></div>
                </div>
            </div>
        </div>

        <div id="mollie-error-message" class="error-message" style="display: none;"></div>
    </div>

    <button type="submit" id="submit-button">
        <span id="button-text">Complete Order</span>
        <span id="button-spinner" class="spinner" style="display: none;"></span>
    </button>
</form>

Step 3: Initialize Mollie Components

Initialize Mollie.js and create components:
async function setupMollieCheckout() {
    try {
        // Get Mollie profile ID from payment context
        const context = await createMolliePaymentContext();

        // Initialize Mollie object
        const mollie = Mollie(context.profile_id, {
            locale: 'en_US',
            testmode: context.testmode || false
        });

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

        // Mount components to DOM
        components.cardHolder.mount('#card-holder');
        components.cardNumber.mount('#card-number');
        components.expiryDate.mount('#expiry-date');
        components.verificationCode.mount('#verification-code');

        // Setup error handling
        setupComponentErrors(components);

        // Setup form submission
        setupFormSubmission(mollie, components);

        console.log('Mollie checkout initialized successfully');

    } catch (error) {
        console.error('Mollie setup error:', error);
        showError('Payment setup failed. Please refresh and try again.');
    }
}

async function createMolliePaymentContext() {
    const cartKey = localStorage.getItem('cart_key');

    const response = await fetch('/wp-json/cocart/preview/checkout/payment-context', {
        method: 'POST',
        headers: {
            'Cart-Key': cartKey,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            payment_method: 'mollie'
        })
    });

    if (!response.ok) {
        const error = await response.json();
        throw new Error(error.message || 'Failed to create payment context');
    }

    const context = await response.json();

    // Context contains:
    // - profile_id: Your Mollie profile ID
    // - testmode: Boolean indicating test mode
    // - locale: Preferred locale (e.g., 'en_US', 'nl_NL')

    return context;
}

Step 4: Component Error Handling

Set up validation and error handling for components:
function setupComponentErrors(components) {
    // Listen for errors on each component
    Object.keys(components).forEach(key => {
        const component = components[key];
        const errorElement = document.getElementById(`${getElementId(key)}-error`);

        component.addEventListener('change', event => {
            if (event.error && event.touched) {
                errorElement.textContent = event.error;
                errorElement.style.display = 'block';
            } else {
                errorElement.textContent = '';
                errorElement.style.display = 'none';
            }
        });

        component.addEventListener('focus', () => {
            errorElement.textContent = '';
            errorElement.style.display = 'none';
        });
    });
}

function getElementId(componentKey) {
    const mapping = {
        cardHolder: 'card-holder',
        cardNumber: 'card-number',
        expiryDate: 'expiry-date',
        verificationCode: 'verification-code'
    };
    return mapping[componentKey] || componentKey;
}

Step 5: Handle Form Submission

Process the checkout when the user submits the form:
function setupFormSubmission(mollie, components) {
    const form = document.getElementById('checkout-form');
    const submitButton = document.getElementById('submit-button');
    const buttonText = document.getElementById('button-text');
    const buttonSpinner = document.getElementById('button-spinner');

    form.addEventListener('submit', async (event) => {
        event.preventDefault();

        // Disable submit button
        submitButton.disabled = true;
        buttonText.textContent = 'Processing...';
        buttonSpinner.style.display = 'inline-block';

        try {
            // Validate form fields
            if (!validateForm()) {
                throw new Error('Please fill in all required fields correctly.');
            }

            // Get form data
            const formData = new FormData(form);
            const billingAddress = getBillingAddressFromForm(formData);

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

            if (error) {
                throw new Error(error.message || 'Card validation failed.');
            }

            // Prepare payment data with token
            const paymentData = {
                card_token: token,
                card_holder: formData.get('billing_first_name') + ' ' + formData.get('billing_last_name')
            };

            // Process checkout
            await processMollieCheckout(billingAddress, paymentData);

        } catch (error) {
            console.error('Checkout error:', error);
            showError(error.message || 'Checkout failed. Please try again.');
        } finally {
            // Re-enable submit button
            submitButton.disabled = false;
            buttonText.textContent = 'Complete Order';
            buttonSpinner.style.display = 'none';
        }
    });
}

// Helper function to get billing address from form
function getBillingAddressFromForm(formData) {
    return {
        first_name: formData.get('billing_first_name'),
        last_name: formData.get('billing_last_name'),
        email: formData.get('billing_email'),
        phone: formData.get('billing_phone'),
        address_1: formData.get('billing_address_1'),
        city: formData.get('billing_city'),
        state: formData.get('billing_state'),
        postcode: formData.get('billing_postcode'),
        country: formData.get('billing_country')
    };
}

// Helper function to validate form
function validateForm() {
    const form = document.getElementById('checkout-form');
    const requiredFields = form.querySelectorAll('[required]');
    let isValid = true;

    requiredFields.forEach(field => {
        if (!field.value.trim()) {
            isValid = false;
            field.classList.add('error');
        } else {
            field.classList.remove('error');
        }
    });

    return isValid;
}

Step 6: Process Checkout

Submit the checkout with card token to CoCart:
async function processMollieCheckout(billingAddress, paymentData) {
    const cartKey = localStorage.getItem('cart_key');

    const checkoutData = {
        billing_address: billingAddress,
        payment_method: 'mollie',
        payment_data: paymentData
    };

    const response = await fetch('https://yoursite.com/wp-json/cocart/preview/checkout', {
        method: 'PUT',
        headers: {
            'Cart-Key': cartKey,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(checkoutData)
    });

    const result = await response.json();

    if (!response.ok) {
        // Handle specific Mollie errors
        if (result.data?.gateway_error) {
            throw new MollieError(result.data.gateway_error);
        }
        throw new Error(result.message || `HTTP ${response.status}`);
    }

    // Handle successful checkout
    if (result.order_id) {
        showSuccess(`Order #${result.order_number} completed successfully!`);

        // Clear cart
        localStorage.removeItem('cart_key');

        // Redirect to 3D Secure or thank you page
        if (result.payment_result?.redirect_url) {
            setTimeout(() => {
                window.location.href = result.payment_result.redirect_url;
            }, 2000);
        }
    }

    return result;
}

// Custom error class for Mollie errors
class MollieError extends Error {
    constructor(gatewayError) {
        super(gatewayError.message || 'Payment processing failed');
        this.code = gatewayError.code;
        this.field = gatewayError.field;
    }

    getDisplayMessage() {
        // Return user-friendly error messages
        switch (this.code) {
            case 'card_declined':
                return 'Your card was declined. Please try a different card.';
            case 'card_expired':
                return 'Your card has expired. Please use a different card.';
            case 'insufficient_funds':
                return 'Insufficient funds. Please use a different card.';
            case 'invalid_card_number':
                return 'Invalid card number. Please check and try again.';
            case 'invalid_cvv':
                return 'Invalid CVV/CVC code. Please check and try again.';
            case 'invalid_expiry':
                return 'Invalid expiry date. Please check and try again.';
            case 'card_not_supported':
                return 'This card type is not supported. Please use a different card.';
            default:
                return this.message || 'Payment processing failed. Please try again.';
        }
    }
}

Complete Integration Example

Here’s a complete working implementation:
class MollieCheckout {
    constructor() {
        this.mollie = null;
        this.components = {};
        this.formValid = false;
    }

    async initialize() {
        try {
            // Create payment context
            const context = await this.createPaymentContext();

            // Initialize Mollie
            this.mollie = Mollie(context.profile_id, {
                locale: context.locale || 'en_US',
                testmode: context.testmode || false
            });

            // Create and mount components
            this.createComponents();

            // Setup validation
            this.setupValidation();

            // Setup form submission
            this.setupFormSubmission();

            console.log('Mollie checkout initialized successfully');
        } catch (error) {
            console.error('Mollie initialization error:', error);
            this.showError('Payment system unavailable. Please try again later.');
        }
    }

    async createPaymentContext() {
        const cartKey = localStorage.getItem('cart_key');

        const response = await fetch('/wp-json/cocart/preview/checkout/payment-context', {
            method: 'POST',
            headers: {
                'Cart-Key': cartKey,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ payment_method: 'mollie' })
        });

        const context = await response.json();

        if (!response.ok) {
            throw new Error(context.message || 'Failed to create payment context');
        }

        return context;
    }

    createComponents() {
        // Create components
        this.components = {
            cardHolder: this.mollie.createComponent('cardHolder'),
            cardNumber: this.mollie.createComponent('cardNumber'),
            expiryDate: this.mollie.createComponent('expiryDate'),
            verificationCode: this.mollie.createComponent('verificationCode')
        };

        // Mount components
        this.components.cardHolder.mount('#card-holder');
        this.components.cardNumber.mount('#card-number');
        this.components.expiryDate.mount('#expiry-date');
        this.components.verificationCode.mount('#verification-code');
    }

    setupValidation() {
        const componentIds = {
            cardHolder: 'card-holder',
            cardNumber: 'card-number',
            expiryDate: 'expiry-date',
            verificationCode: 'verification-code'
        };

        Object.keys(this.components).forEach(key => {
            const component = this.components[key];
            const errorElement = document.getElementById(`${componentIds[key]}-error`);

            component.addEventListener('change', event => {
                if (event.error && event.touched) {
                    errorElement.textContent = event.error;
                    errorElement.style.display = 'block';
                } else {
                    errorElement.textContent = '';
                    errorElement.style.display = 'none';
                }
            });

            component.addEventListener('focus', () => {
                errorElement.textContent = '';
                errorElement.style.display = 'none';
            });
        });

        // Setup form field validation
        const form = document.getElementById('checkout-form');
        const inputs = form.querySelectorAll('input[required]');

        inputs.forEach(input => {
            input.addEventListener('blur', () => this.updateFormValidity());
            input.addEventListener('input', () => this.updateFormValidity());
        });

        this.updateFormValidity();
    }

    updateFormValidity() {
        const form = document.getElementById('checkout-form');
        const requiredFields = form.querySelectorAll('input[required]');
        let isValid = true;

        requiredFields.forEach(field => {
            if (!field.value.trim()) {
                isValid = false;
            }
        });

        this.formValid = isValid;
    }

    setupFormSubmission() {
        const form = document.getElementById('checkout-form');

        form.addEventListener('submit', async (event) => {
            event.preventDefault();

            const submitButton = form.querySelector('[type="submit"]');
            const originalText = submitButton.textContent;

            try {
                submitButton.disabled = true;
                submitButton.textContent = 'Processing...';

                if (!this.formValid) {
                    throw new Error('Please correct the errors in your form.');
                }

                const formData = new FormData(form);
                const billingAddress = this.getBillingAddressFromForm(formData);

                // Create card token
                const { token, error } = await this.mollie.createToken();

                if (error) {
                    throw new Error(error.message || 'Card validation failed.');
                }

                const paymentData = {
                    card_token: token,
                    card_holder: `${billingAddress.first_name} ${billingAddress.last_name}`
                };

                // Process checkout
                await this.processCheckout(billingAddress, paymentData);

            } catch (error) {
                console.error('Checkout error:', error);

                if (error instanceof MollieError) {
                    this.showError(error.getDisplayMessage());
                } else {
                    this.showError(error.message || 'Checkout failed. Please try again.');
                }
            } finally {
                submitButton.disabled = false;
                submitButton.textContent = originalText;
            }
        });
    }

    async processCheckout(billingAddress, paymentData) {
        const cartKey = localStorage.getItem('cart_key');

        const response = await fetch('/wp-json/cocart/preview/checkout', {
            method: 'PUT',
            headers: {
                'Cart-Key': cartKey,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                billing_address: billingAddress,
                payment_method: 'mollie',
                payment_data: paymentData
            })
        });

        const result = await response.json();

        if (!response.ok) {
            if (result.data?.gateway_error) {
                throw new MollieError(result.data.gateway_error);
            }
            throw new Error(result.message || `HTTP ${response.status}`);
        }

        this.handleCheckoutSuccess(result);
        return result;
    }

    getBillingAddressFromForm(formData) {
        return {
            first_name: formData.get('billing_first_name'),
            last_name: formData.get('billing_last_name'),
            email: formData.get('billing_email'),
            phone: formData.get('billing_phone'),
            address_1: formData.get('billing_address_1'),
            city: formData.get('billing_city'),
            state: formData.get('billing_state'),
            postcode: formData.get('billing_postcode'),
            country: formData.get('billing_country')
        };
    }

    handleCheckoutSuccess(result) {
        this.showSuccess(`Order #${result.order_number} completed successfully!`);

        // Clear cart
        localStorage.removeItem('cart_key');

        // Redirect to 3D Secure or thank you page
        if (result.payment_result?.redirect_url) {
            setTimeout(() => {
                window.location.href = result.payment_result.redirect_url;
            }, 2000);
        }
    }

    showError(message) {
        const errorElement = document.getElementById('mollie-error-message');
        errorElement.textContent = message;
        errorElement.style.display = 'block';
        errorElement.className = 'error-message';
    }

    showSuccess(message) {
        const errorElement = document.getElementById('mollie-error-message');
        errorElement.textContent = message;
        errorElement.style.display = 'block';
        errorElement.className = 'success-message';
    }
}

// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', async () => {
    const checkout = new MollieCheckout();
    await checkout.initialize();
});

Styling Components

Add CSS to style the Mollie Components:
/* Mollie Components Containers */
.mollie-component {
    height: 40px;
    padding: 10px;
    border: 1px solid #cbd5e0;
    border-radius: 4px;
    background-color: white;
    transition: border-color 0.2s ease;
}

.mollie-component:focus-within {
    border-color: #4299e1;
    outline: none;
    box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1);
}

/* Form Groups */
.form-group {
    margin-bottom: 1rem;
}

.form-group label {
    display: block;
    margin-bottom: 0.5rem;
    font-size: 0.875rem;
    font-weight: 500;
    color: #4a5568;
}

/* Card Row Layout */
.card-row {
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: 1rem;
}

/* Error Messages */
.error-message {
    margin-top: 0.5rem;
    font-size: 0.875rem;
    color: #e53e3e;
    display: none;
}

.error-message:not(:empty) {
    display: block;
}

#mollie-error-message.error-message {
    margin-top: 1rem;
    padding: 0.75rem;
    background-color: #fed7d7;
    border-left: 4px solid #e53e3e;
    color: #742a2a;
    border-radius: 4px;
}

#mollie-error-message.success-message {
    margin-top: 1rem;
    padding: 0.75rem;
    background-color: #c6f6d5;
    border-left: 4px solid #38a169;
    color: #22543d;
    border-radius: 4px;
}

/* Input validation states */
input.error {
    border-color: #e53e3e;
}

input.valid {
    border-color: #38a169;
}

Testing

For development and testing with Mollie:

Test Mode

Enable test mode when initializing Mollie:
const mollie = Mollie(context.profile_id, {
    testmode: true,
    locale: 'en_US'
});

Test Card Numbers

Use these test cards in test mode:
  • Valid Card: 5555 5555 5555 4444 (Mastercard)
  • Declined Card: 5555 5555 5555 4440
  • Insufficient Funds: 5555 5555 5555 4442
  • Expired Card: 5555 5555 5555 4445

Test Card Details

  • Expiry Date: Any future date (e.g., 12/25)
  • CVC: Any 3 digits (e.g., 123)
  • Cardholder: Any name

3D Secure Testing

Mollie automatically handles 3D Secure authentication. In test mode, you’ll be redirected to a test authentication page.

Error Handling

Handle common Mollie error scenarios:
function handleMollieErrors(error) {
    // Common Mollie error codes
    const errorMessages = {
        'card_declined': 'Card declined by issuer',
        'card_expired': 'Card has expired',
        'insufficient_funds': 'Insufficient funds on card',
        'invalid_card_number': 'Invalid card number',
        'invalid_cvv': 'Invalid CVV/CVC code',
        'invalid_expiry': 'Invalid expiration date',
        'card_not_supported': 'Card type not supported',
        'authentication_failed': '3D Secure authentication failed',
        'card_lost_stolen': 'Card reported lost or stolen',
        'restricted_card': 'Card is restricted',
        'duplicate_transaction': 'Duplicate transaction detected'
    };

    return errorMessages[error.code] || error.message || 'Payment processing failed';
}

Best Practices

Security

  • Always use Mollie Components for card data
  • Never store raw card information
  • Use HTTPS for all requests
  • Implement proper form validation
  • Card tokens expire after 1 hour
  • Handle 3D Secure redirects properly

User Experience

  • Show real-time validation errors
  • Provide clear error messages
  • Handle declined cards gracefully
  • Support keyboard navigation
  • Display supported card brands
  • Auto-format card numbers

Compliance

  • Mollie Components provide SAQ A compliance
  • No sensitive card data touches your server
  • Implement proper error handling
  • Log transactions for auditing
  • Test with various card types
  • Follow GDPR requirements

Performance

  • Load Mollie.js from CDN
  • Components auto-update
  • Implement proper timeouts
  • Handle network failures
  • Monitor transaction success rates
  • Cache payment contexts appropriately

Supported Payment Methods

Beyond credit cards, Mollie supports 30+ payment methods:

European Payment Methods

  • iDEAL (Netherlands)
  • Bancontact (Belgium)
  • SOFORT (Germany, Austria)
  • Giropay (Germany)
  • EPS (Austria)
  • Przelewy24 (Poland)
  • KBC/CBC (Belgium)
  • Belfius (Belgium)

Other Methods

  • PayPal
  • Apple Pay
  • Credit Card (Visa, Mastercard, Amex)
  • SEPA Direct Debit
  • Bank Transfer
  • Gift Cards (Various brands)
Each payment method has its own component or API integration. Consult the Mollie documentation for specific implementation details.

Advanced Features

Apple Pay Integration

// Check if Apple Pay is available
if (mollie.supportsApplePay()) {
    // Create Apple Pay button
    const applePayButton = mollie.createComponent('applePayButton', {
        amount: {
            currency: 'EUR',
            value: '10.00'
        },
        countryCode: 'NL'
    });

    applePayButton.mount('#apple-pay-button');

    applePayButton.on('authorized', async (event) => {
        // Process payment with event.token
    });
}

iDEAL Bank Selection

// For iDEAL payments, create issuer selector
const idealIssuer = mollie.createComponent('idealIssuer');
idealIssuer.mount('#ideal-issuer-select');

Troubleshooting

Common issues and solutions: Components not loading: Check that Mollie.js script is loaded and profile ID is correct. Card token creation fails: Verify all component fields are valid and filled. 3D Secure redirect fails: Ensure your redirect URLs are properly configured in Mollie dashboard. Styling not applied: Components use iframes; customize using Mollie’s styling API. Test mode not working: Verify testmode flag is set to true in Mollie initialization.
Always test your Mollie integration thoroughly using test mode before going live. Ensure your webhook endpoints are configured to handle payment status updates. Monitor your Mollie dashboard for declined transactions and implement appropriate retry logic.

Webhook Handling

Mollie sends webhooks for payment status updates:
// Server-side webhook handler (example)
app.post('/webhooks/mollie', async (req, res) => {
    const paymentId = req.body.id;

    // Fetch payment status from Mollie API
    const payment = await mollieClient.payments.get(paymentId);

    if (payment.status === 'paid') {
        // Update order status
        await updateOrderStatus(payment.metadata.order_id, 'paid');
    } else if (payment.status === 'failed') {
        // Handle failed payment
        await updateOrderStatus(payment.metadata.order_id, 'failed');
    }

    res.sendStatus(200);
});

Additional Resources

I