Mollie Subscription API for Shopify Developers: Building Custom Recurring Logic
Meta Description: Learn to build custom subscription flows using the Mollie Subscription API with Shopify. Complete developer guide with code examples, webhook handling, and API integration patterns for advanced recurring billing.
When Standard Subscription Apps Aren't Enough
Shopify's subscription ecosystem has a ceiling. Apps like ReCharge, Bold Subscriptions, and even Mollie's native integration work beautifully—until they don't. Maybe you need to sync subscription data with an external CRM. Perhaps you want to offer complex billing logic that no app supports. Or your client needs a custom customer portal that doesn't exist in any off-the-shelf solution.
That's when you need to go deeper. The Mollie Subscription API gives you direct access to subscription lifecycle management, letting you build exactly what your project demands without fighting against app limitations.
This guide is for developers who need more than configuration options. We'll build a custom subscription integration from scratch, handling webhooks, payment mandates, and recurring payment logic with production-ready code examples.
Understanding the Mollie Subscription Architecture
Before writing code, you need to understand how Mollie's subscription system works under the hood.
Core Concepts
Customer: A Mollie customer object that holds payment mandates and subscription history. One Shopify customer maps to one Mollie customer.
Mandate: A payment authorization that allows Mollie to charge a customer repeatedly. For SEPA, this is a direct debit mandate. For cards, it's a stored credential agreement.
Subscription: A recurring payment schedule attached to a customer with a valid mandate. The subscription defines when and how much to charge.
Payment: An individual transaction generated by a subscription. Each billing cycle creates a new payment object.
The Subscription Lifecycle
Customer Created → Mandate Established → Subscription Created → Payments Generated (recurring)
↓
Payment Failed → Retry Logic → Cancellation (after max retries)API Authentication
All Mollie API requests require an API key in the Authorization header:
curl https://api.mollie.com/v2/subscriptions \
-H "Authorization: Bearer test_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"Test keys start with test_, live keys with live_. Never expose live keys in client-side code or version control.
Setting Up Your Development Environment
Required Dependencies
For a Node.js/Express Shopify app:
{
"dependencies": {
"@mollie/api-client": "^3.7.0",
"@shopify/shopify-api": "^9.0.0",
"express": "^4.18.0",
"crypto": "^1.0.1",
"dotenv": "^16.0.0"
}
}Install with:
npm install @mollie/api-client @shopify/shopify-api express crypto dotenvEnvironment Configuration
Create a .env file:
# Mollie Configuration
MOLLIE_TEST_API_KEY=test_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
MOLLIE_LIVE_API_KEY=live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
MOLLIE_WEBHOOK_SECRET=your_webhook_signing_secret
# Shopify Configuration
SHOPIFY_API_KEY=your_shopify_api_key
SHOPIFY_API_SECRET=your_shopify_secret
SHOPIFY_APP_URL=https://your-app.herokuapp.com
# Application
NODE_ENV=development
PORT=3000Load environment variables in your entry file:
require('dotenv').config();
const createMollieClient = require('@mollie/api-client').default;
const mollieClient = createMollieClient({
apiKey: process.env.NODE_ENV === 'production'
? process.env.MOLLIE_LIVE_API_KEY
: process.env.MOLLIE_TEST_API_KEY
});
module.exports = { mollieClient };Creating Customers and Mandates
Before creating subscriptions, you need customers with valid payment mandates.
Step 1: Create a Mollie Customer
When a Shopify customer initiates their first subscription, create a corresponding Mollie customer:
const { mollieClient } = require('./config/mollie');
async function createMollieCustomer(shopifyCustomer) {
try {
const customer = await mollieClient.customers.create({
name: `${shopifyCustomer.first_name} ${shopifyCustomer.last_name}`,
email: shopifyCustomer.email,
metadata: {
shopify_customer_id: shopifyCustomer.id.toString(),
shopify_shop_domain: shopifyCustomer.shop_domain
}
});
// Store the mapping in your database
await db.customers.insert({
shopify_customer_id: shopifyCustomer.id,
mollie_customer_id: customer.id,
created_at: new Date()
});
return customer;
} catch (error) {
console.error('Failed to create Mollie customer:', error);
throw error;
}
}Step 2: Create a Payment for First Checkout (Mandate Creation)
The first payment establishes the mandate. Mollie handles the complexity of mandate creation based on the payment method:
async function createFirstPaymentWithMandate(mollieCustomerId, orderDetails) {
try {
const payment = await mollieClient.payments.create({
amount: {
currency: 'EUR',
value: orderDetails.amount.toFixed(2)
},
customerId: mollieCustomerId,
description: `First subscription payment - ${orderDetails.subscription_name}`,
redirectUrl: `${process.env.SHOPIFY_APP_URL}/subscription/success`,
webhookUrl: `${process.env.SHOPIFY_APP_URL}/webhooks/mollie`,
metadata: {
order_id: orderDetails.order_id,
subscription_type: orderDetails.subscription_type,
is_first_payment: 'true'
},
// Sequence type indicates this is the first in a series
sequenceType: 'first'
});
return payment;
} catch (error) {
console.error('Failed to create first payment:', error);
throw error;
}
}Step 3: Check Mandate Status
After the first payment completes, verify the mandate was created:
async function getCustomerMandates(mollieCustomerId) {
try {
const mandates = await mollieClient.customerMandates.list({
customerId: mollieCustomerId
});
// Return only valid mandates
return mandates.filter(mandate => mandate.status === 'valid');
} catch (error) {
console.error('Failed to fetch mandates:', error);
throw error;
}
}Creating and Managing Subscriptions
With a customer and valid mandate, you can create subscriptions.
Creating a Subscription
async function createSubscription(mollieCustomerId, subscriptionDetails) {
try {
const subscription = await mollieClient.subscriptions.create({
customerId: mollieCustomerId,
amount: {
currency: 'EUR',
value: subscriptionDetails.amount.toFixed(2)
},
// Interval format: 1 month, 2 weeks, 3 days, etc.
interval: subscriptionDetails.interval,
description: subscriptionDetails.description,
webhookUrl: `${process.env.SHOPIFY_APP_URL}/webhooks/mollie`,
metadata: {
shopify_order_id: subscriptionDetails.order_id,
shopify_product_id: subscriptionDetails.product_id,
subscription_tier: subscriptionDetails.tier
},
// Optional: start date (defaults to now)
startDate: subscriptionDetails.start_date || new Date().toISOString().split('T')[0],
// Optional: mandate ID (if customer has multiple)
mandateId: subscriptionDetails.mandate_id
});
// Store subscription reference
await db.subscriptions.insert({
mollie_subscription_id: subscription.id,
mollie_customer_id: mollieCustomerId,
shopify_order_id: subscriptionDetails.order_id,
status: 'active',
interval: subscriptionDetails.interval,
amount: subscriptionDetails.amount,
created_at: new Date()
});
return subscription;
} catch (error) {
console.error('Failed to create subscription:', error);
throw error;
}
}Retrieving Subscriptions
async function getCustomerSubscriptions(mollieCustomerId) {
try {
const subscriptions = await mollieClient.subscriptions.list({
customerId: mollieCustomerId
});
return subscriptions;
} catch (error) {
console.error('Failed to fetch subscriptions:', error);
throw error;
}
}
async function getSubscriptionDetails(mollieCustomerId, subscriptionId) {
try {
const subscription = await mollieClient.subscriptions.get({
customerId: mollieCustomerId,
subscriptionId: subscriptionId
});
return subscription;
} catch (error) {
console.error('Failed to fetch subscription:', error);
throw error;
}
}Updating a Subscription
async function updateSubscription(mollieCustomerId, subscriptionId, updates) {
try {
const subscription = await mollieClient.subscriptions.update({
customerId: mollieCustomerId,
subscriptionId: subscriptionId,
amount: updates.amount ? {
currency: 'EUR',
value: updates.amount.toFixed(2)
} : undefined,
description: updates.description,
interval: updates.interval,
webhookUrl: `${process.env.SHOPIFY_APP_URL}/webhooks/mollie`,
metadata: updates.metadata
});
// Update local record
await db.subscriptions.update(
{ mollie_subscription_id: subscriptionId },
{
amount: updates.amount,
interval: updates.interval,
updated_at: new Date()
}
);
return subscription;
} catch (error) {
console.error('Failed to update subscription:', error);
throw error;
}
}Cancelling a Subscription
async function cancelSubscription(mollieCustomerId, subscriptionId) {
try {
await mollieClient.subscriptions.delete({
customerId: mollieCustomerId,
subscriptionId: subscriptionId
});
// Update local record
await db.subscriptions.update(
{ mollie_subscription_id: subscriptionId },
{
status: 'cancelled',
cancelled_at: new Date()
}
);
return { success: true };
} catch (error) {
console.error('Failed to cancel subscription:', error);
throw error;
}
}Handling Webhooks: The Critical Piece
Webhooks are how Mollie notifies your application of subscription events. Getting this wrong means missed payments and angry customers.
Webhook Endpoint Setup
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
// Verify webhook signature
function verifyWebhookSignature(requestBody, signature) {
const hmac = crypto.createHmac('sha256', process.env.MOLLIE_WEBHOOK_SECRET);
hmac.update(requestBody);
const digest = hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(digest),
Buffer.from(signature)
);
}
router.post('/webhooks/mollie', express.raw({ type: 'application/json' }), async (req, res) => {
// Immediately return 200 to acknowledge receipt
res.status(200).send('OK');
const signature = req.headers['x-mollie-signature'];
if (!verifyWebhookSignature(req.body, signature)) {
console.error('Invalid webhook signature');
return;
}
const event = JSON.parse(req.body);
try {
await handleWebhookEvent(event);
} catch (error) {
console.error('Webhook processing failed:', error);
// Log to monitoring system
}
});
async function handleWebhookEvent(event) {
const { event: eventType, data } = event;
switch (eventType) {
case 'subscription.created':
await handleSubscriptionCreated(data);
break;
case 'subscription.updated':
await handleSubscriptionUpdated(data);
break;
case 'subscription.canceled':
await handleSubscriptionCanceled(data);
break;
case 'payment.paid':
await handlePaymentPaid(data);
break;
case 'payment.failed':
await handlePaymentFailed(data);
break;
case 'payment.chargeback':
await handleChargeback(data);
break;
default:
console.log(`Unhandled event type: ${eventType}`);
}
}Payment Success Handler
async function handlePaymentPaid(paymentData) {
const { id, subscriptionId, customerId, amount } = paymentData;
// Find the corresponding subscription in your database
const subscription = await db.subscriptions.findOne({
mollie_subscription_id: subscriptionId
});
if (!subscription) {
console.error(`Subscription ${subscriptionId} not found`);
return;
}
// Create a Shopify order for this payment
const shopifyOrder = await createShopifyOrder({
customer_id: subscription.shopify_customer_id,
amount: amount.value,
currency: amount.currency,
subscription_id: subscriptionId,
payment_id: id
});
// Log the successful payment
await db.payments.insert({
mollie_payment_id: id,
mollie_subscription_id: subscriptionId,
shopify_order_id: shopifyOrder.id,
amount: amount.value,
currency: amount.currency,
status: 'paid',
paid_at: new Date()
});
// Send confirmation email to customer
await sendPaymentConfirmation(subscription.shopify_customer_id, {
amount: amount.value,
currency: amount.currency,
order_id: shopifyOrder.id
});
}Payment Failure Handler (Dunning)
async function handlePaymentFailed(paymentData) {
const { id, subscriptionId, customerId, amount } = paymentData;
const subscription = await db.subscriptions.findOne({
mollie_subscription_id: subscriptionId
});
if (!subscription) return;
// Get failure count for this subscription
const failureCount = await db.payments.count({
mollie_subscription_id: subscriptionId,
status: 'failed'
});
// Log the failed payment
await db.payments.insert({
mollie_payment_id: id,
mollie_subscription_id: subscriptionId,
amount: amount.value,
currency: amount.currency,
status: 'failed',
failed_at: new Date()
});
if (failureCount === 0) {
// First failure: friendly reminder
await sendDunningEmail(subscription.shopify_customer_id, {
type: 'first_reminder',
subscription_id: subscriptionId,
amount: amount.value
});
} else if (failureCount === 1) {
// Second failure: urgent notice
await sendDunningEmail(subscription.shopify_customer_id, {
type: 'urgent',
subscription_id: subscriptionId,
amount: amount.value,
update_payment_url: `${process.env.SHOPIFY_APP_URL}/billing/update/${subscriptionId}`
});
} else if (failureCount >= 2) {
// Third failure: cancellation warning
await sendDunningEmail(subscription.shopify_customer_id, {
type: 'final_notice',
subscription_id: subscriptionId,
amount: amount.value,
cancellation_date: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString()
});
// Optionally cancel the subscription
// await cancelSubscription(customerId, subscriptionId);
}
}Integration with Shopify
Custom App Setup
To make this work within Shopify, you need a custom app with the right permissions:
- In your Shopify Partner dashboard, create a new app
- Set these API access scopes:
read_orders,write_ordersread_customers,write_customersread_products,write_productsread_draft_orders,write_draft_orders
Creating Shopify Orders from Subscription Payments
const { Shopify } = require('@shopify/shopify-api');
async function createShopifyOrder(paymentDetails) {
const client = new Shopify.Clients.Graphql(
paymentDetails.shop_domain,
paymentDetails.access_token
);
const mutation = `
mutation draftOrderCreate($input: DraftOrderInput!) {
draftOrderCreate(input: $input) {
draftOrder {
id
order {
id
name
}
}
userErrors {
field
message
}
}
}
`;
const variables = {
input: {
customerId: paymentDetails.customer_gid,
note: `Subscription payment: ${paymentDetails.payment_id}`,
tags: ['subscription', 'recurring'],
lineItems: [
{
variantId: paymentDetails.variant_gid,
quantity: 1,
originalUnitPrice: paymentDetails.amount
}
],
metafields: [
{
namespace: 'subscription',
key: 'mollie_payment_id',
value: paymentDetails.payment_id,
type: 'single_line_text_field'
}
]
}
};
const response = await client.query({
data: { query: mutation, variables }
});
// Complete the draft order to create a real order
const draftOrderId = response.body.data.draftOrderCreate.draftOrder.id;
await completeDraftOrder(client, draftOrderId);
return response.body.data.draftOrderCreate.draftOrder.order;
}
async function completeDraftOrder(client, draftOrderId) {
const mutation = `
mutation draftOrderComplete($id: ID!) {
draftOrderComplete(id: $id) {
draftOrder {
order {
id
name
}
}
}
}
`;
await client.query({
data: {
query: mutation,
variables: { id: draftOrderId }
}
});
}Error Handling and Edge Cases
Idempotency
Always implement idempotency for webhook handlers to prevent duplicate processing:
async function handlePaymentPaid(paymentData) {
// Check if we've already processed this payment
const existing = await db.payments.findOne({
mollie_payment_id: paymentData.id
});
if (existing && existing.status === 'paid') {
console.log(`Payment ${paymentData.id} already processed`);
return;
}
// Process payment...
}Mandate Failures
Sometimes mandates fail (expired cards, cancelled bank accounts). Handle this gracefully:
async function handleMandateRevoked(mandateData) {
const { id, customerId } = mandateData;
// Find all active subscriptions using this mandate
const subscriptions = await db.subscriptions.find({
mollie_customer_id: customerId,
status: 'active'
});
// Pause subscriptions and notify customer
for (const sub of subscriptions) {
await pauseSubscription(sub.mollie_subscription_id);
await sendMandateUpdateRequired(sub.shopify_customer_id);
}
}Production Checklist
Before going live:
- [ ] Webhook endpoint accessible from public internet (use ngrok for local testing)
- [ ] Webhook signature verification implemented
- [ ] Idempotency keys on all critical operations
- [ ] Comprehensive logging and monitoring
- [ ] Error alerting (PagerDuty, Slack, etc.)
- [ ] Database migrations for subscription tracking tables
- [ ] Test mode thoroughly validated
- [ ] GDPR compliance (data retention policies)
- [ ] PCI DSS scope assessment (Mollie handles most, but verify)
Conclusion: Beyond the App Store
Building custom subscription logic with the Mollie API isn't the easy path—but it's the path to truly differentiated commerce experiences. When standard apps force compromises, direct API access gives you control.
What you've built:
- ✅ Direct Mollie API integration with Shopify
- ✅ Customer and mandate management
- ✅ Subscription lifecycle handling
- ✅ Robust webhook processing
- ✅ Payment failure recovery (dunning)
- ✅ Shopify order synchronization
Your next steps:
- Build the customer portal—Let customers manage their own subscriptions
- Add analytics—Track MRR, churn, and lifetime value
- Implement upsells—Use subscription data for targeted offers
- Optimize retry logic—A/B test dunning email timing and copy
The Mollie Subscription API removes the ceiling on what's possible. Now it's your turn to build something that doesn't exist yet.
Need help with a complex integration? We've built custom subscription systems for scaling Shopify merchants. [Let's talk](#).
JSON-LD Structured Data
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Mollie Subscription API for Shopify Developers: Building Custom Recurring Logic",
"description": "Learn to build custom subscription flows using the Mollie Subscription API with Shopify. Complete developer guide with code examples, webhook handling, and API integration patterns.",
"keywords": "mollie api shopify subscriptions, mollie subscription api, shopify custom subscriptions, recurring billing api, mollie webhook integration",
"author": {
"@type": "Organization",
"name": "Subora"
},
"publisher": {
"@type": "Organization",
"name": "Subora",
"logo": {
"@type": "ImageObject",
"url": "https://subora.eu/logo.png"
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://subora.eu/blog/mollie-api-shopify-subscriptions"
},
"datePublished": "2026-04-17",
"dateModified": "2026-04-17",
"articleSection": "Developer Documentation",
"wordCount": 2150,
"proficiencyLevel": "Advanced",
"dependencies": "Shopify, Mollie API, Node.js, Express",
"programmingLanguage": ["JavaScript", "Node.js"],
"codeSampleType": "Full"
}Word Count: ~2,150 words Reading Time: 12 minutes Technical Difficulty: Advanced Last Updated: April 17, 2026
Subora Team
Subscription operators
Practical notes from the team working on Shopify subscriptions, recurring billing, and subscriber self-service flows.
Relevant product lane
Native Shopify subscriptions for European recurring revenue.
Explore Subora
