Back to blog
Shopify Subscriptions17 april 20268 min read

Mollie Subscription API for Shopify Developers: Building Custom Recurring Logic

Subscriptions

Published

17 april 2026

Updated

17 april 2026

Category

Shopify Subscriptions

Author

Subora Team

Focus

Subscriptions

Mollie Subscription API for Shopify Developers: Building Custom Recurring Logic

On this page

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 dotenv

Environment 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=3000

Load 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:

  1. In your Shopify Partner dashboard, create a new app
  2. Set these API access scopes:
  • read_orders, write_orders
  • read_customers, write_customers
  • read_products, write_products
  • read_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:

  1. Build the customer portal—Let customers manage their own subscriptions
  2. Add analytics—Track MRR, churn, and lifetime value
  3. Implement upsells—Use subscription data for targeted offers
  4. 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
Need help applying this?

Turn the note into a working subscription system.

If this article maps to a live bottleneck in your Shopify subscription stack, we can help scope the billing flow, subscriber journey, and implementation path.

More reading

Continue with adjacent subscription notes.

Read the next article in the same layer of the stack, then decide what should be fixed first.

Current layer: Shopify SubscriptionsSubscriptions