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:
Mollie payment gateway configured in WooCommerce
Mollie.js library loaded in your frontend
A valid cart with items added
Customer billing address information
Mollie profile ID (found in Dashboard > Developers > API keys)
Integration Flow
Load Mollie.js
Initialize the Mollie JavaScript library
Initialize Mollie Object
Create Mollie instance with your profile ID
Create Components
Set up secure iframe-based payment input fields
Collect Payment Details
Securely collect card information from customers
Generate Card Token
Create a temporary card token using Mollie.js
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 ;
}
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 : 40 px ;
padding : 10 px ;
border : 1 px solid #cbd5e0 ;
border-radius : 4 px ;
background-color : white ;
transition : border-color 0.2 s ease ;
}
.mollie-component:focus-within {
border-color : #4299e1 ;
outline : none ;
box-shadow : 0 0 0 3 px rgba ( 66 , 153 , 225 , 0.1 );
}
/* Form Groups */
.form-group {
margin-bottom : 1 rem ;
}
.form-group label {
display : block ;
margin-bottom : 0.5 rem ;
font-size : 0.875 rem ;
font-weight : 500 ;
color : #4a5568 ;
}
/* Card Row Layout */
.card-row {
display : grid ;
grid-template-columns : 2 fr 1 fr ;
gap : 1 rem ;
}
/* Error Messages */
.error-message {
margin-top : 0.5 rem ;
font-size : 0.875 rem ;
color : #e53e3e ;
display : none ;
}
.error-message:not ( :empty ) {
display : block ;
}
#mollie-error-message.error-message {
margin-top : 1 rem ;
padding : 0.75 rem ;
background-color : #fed7d7 ;
border-left : 4 px solid #e53e3e ;
color : #742a2a ;
border-radius : 4 px ;
}
#mollie-error-message.success-message {
margin-top : 1 rem ;
padding : 0.75 rem ;
background-color : #c6f6d5 ;
border-left : 4 px solid #38a169 ;
color : #22543d ;
border-radius : 4 px ;
}
/* 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