How to Programmatically Create WooCommerce Subscriptions with Payment Methods

During a recent project, I had to programmatically create WooCommerce subscriptions and link them to existing customer payment methods. I learned a lot through the process, and I thought I would share my findings in this comprehensive tutorial. Whether you’re migrating subscriptions from another platform, or building a custom subscription creation process, this guide will help you achieve your goal.

In this tutorial, we’ll focus on creating subscriptions programmatically and connecting them to Authorize.net payment tokens. However, the same approach can be adapted for other payment gateways with minimal changes.

Prerequisites

  • WooCommerce Subscriptions plugin
  • A payment gateway plugin (we’ll use Authorize.net in this example)
  • Basic understanding of WooCommerce orders and subscriptions

Understanding Required Data

Before we dive into the code, let’s understand what data we need to create a subscription. At minimum, you’ll need:

  • Customer information (ID, billing & shipping details)
  • Subscription details (billing interval, period, start date)
  • Product information (ID, quantity, pricing)
  • Payment method details

The Process

Creating a subscription programmatically involves several steps:

  1. Create a parent order (required for WooCommerce Subscriptions)
  2. Create the subscription and link it to the parent order
  3. Add products and set subscription schedule
  4. Link payment method (optional but recommended)

Implementation

First, let’s create a class to handle our subscription creation:

class WC_Subscription_Creator {
    
    /**
     * Create a new subscription with all required data
     */
    public function create_subscription($subscription_data) {
        // Create parent order first
        $parent_order = wc_create_order([
            'customer_id' => $subscription_data['customer_id'],
            'created_via' => 'subscription_creator',
        ]);

        if (is_wp_error($parent_order)) {
            return $parent_order;
        }

        // Set addresses on parent order
        $parent_order->set_address($subscription_data['billing_info'], 'billing');
        $parent_order->set_address($subscription_data['shipping_info'], 'shipping');

        // Add products to order
        foreach ($subscription_data['products'] as $product_data) {
            $product_id = isset($product_data['variation_id']) ? 
                $product_data['variation_id'] : 
                $product_data['product_id'];

            $product = wc_get_product($product_id);
            if (!$product) {
                continue;
            }

            $parent_order->add_product(
                $product,
                $product_data['quantity'],
                [
                    'total' => $product_data['price'] * $product_data['quantity'],
                    'subtotal' => $product_data['price'] * $product_data['quantity']
                ]
            );
        }

        $parent_order->calculate_totals();
        $parent_order->save();

        // Create the subscription
        $subscription = wcs_create_subscription([
            'status'           => 'pending', // We'll activate it later
            'order_id'        => $parent_order->get_id(),
            'customer_id'     => $subscription_data['customer_id'],
            'billing_period'  => $subscription_data['billing_period'],    // day, week, month or year
            'billing_interval'=> $subscription_data['billing_interval'],  // billing period count
            'start_date'      => $subscription_data['start_date'],
            'created_via'     => 'subscription_creator'
        ]);

        if (is_wp_error($subscription)) {
            return $subscription;
        }

        // Set subscription addresses
        $subscription->set_address($subscription_data['billing_info'], 'billing');
        $subscription->set_address($subscription_data['shipping_info'], 'shipping');

        // Add products to subscription
        foreach ($subscription_data['products'] as $product_data) {
            $product_id = isset($product_data['variation_id']) ? 
                $product_data['variation_id'] : 
                $product_data['product_id'];

            $product = wc_get_product($product_id);
            if (!$product) {
                continue;
            }

            $subscription->add_product(
                $product,
                $product_data['quantity'],
                [
                    'total' => $product_data['price'] * $product_data['quantity'],
                    'subtotal' => $product_data['price'] * $product_data['quantity']
                ]
            );
        }

        // Set subscription dates
        if (!empty($subscription_data['next_payment_date'])) {
            $subscription->update_dates([
                'next_payment' => $subscription_data['next_payment_date']
            ]);
        }

        $subscription->calculate_totals();
        $subscription->save();

        return $subscription;
    }

    /**
     * Get customer's default payment token for Authorize.net
     */
    public function get_payment_token($customer_id) {
        global $wpdb;

        // Check if we're in test mode
        $authorize_settings = get_option('woocommerce_authorize_net_cim_credit_card_settings');
        $is_test = ($authorize_settings['environment'] ?? '') === 'test';

        // Get customer profile ID based on environment
        $profile_meta_key = $is_test ? 
            'wc_authorize_net_cim_customer_profile_id_test' : 
            'wc_authorize_net_cim_customer_profile_id';

        $has_profile = get_user_meta($customer_id, $profile_meta_key, true);
        if (!$has_profile) {
            return false;
        }

        // Get default payment token
        $token_id = $wpdb->get_var($wpdb->prepare(
            "SELECT token_id 
            FROM {$wpdb->prefix}woocommerce_payment_tokens 
            WHERE user_id = %d 
            AND gateway_id = 'authorize_net_cim_credit_card'
            AND is_default = 1
            LIMIT 1",
            $customer_id
        ));

        return $token_id ? WC_Payment_Tokens::get($token_id) : false;
    }

    /**
     * Link payment method to order and subscription
     */
    public function link_payment_method($order, $subscription, $customer_id) {
        $payment_token = $this->get_payment_token($customer_id);
        if (!$payment_token) {
            return false;
        }

        // Get correct profile ID based on environment
        $authorize_settings = get_option('woocommerce_authorize_net_cim_credit_card_settings');
        $is_test = ($authorize_settings['environment'] ?? '') === 'test';
        
        $profile_meta_key = $is_test ? 
            'wc_authorize_net_cim_customer_profile_id_test' : 
            'wc_authorize_net_cim_customer_profile_id';
            
        $customer_profile_id = get_user_meta(
            $customer_id,
            $profile_meta_key,
            true
        );

        // Set payment method on order
        $order->set_payment_method('authorize_net_cim_credit_card');
        $order->set_payment_method_title('Credit Card');
        $order->update_meta_data('_wc_authorize_net_cim_credit_card_payment_token', $payment_token->get_token());
        $order->update_meta_data('_wc_authorize_net_cim_credit_card_customer_id', $customer_profile_id);
        $order->save();

        // Set payment method on subscription
        $subscription->set_payment_method('authorize_net_cim_credit_card');
        $subscription->set_payment_method_title('Credit Card');
        $subscription->update_meta_data('_wc_authorize_net_cim_credit_card_payment_token', $payment_token->get_token());
        $subscription->update_meta_data('_wc_authorize_net_cim_credit_card_customer_id', $customer_profile_id);
        $subscription->save();

        return true;
    }
}

Now that we have our class ready, here’s how to use it:

// Prepare your subscription data
$subscription_data = [
    'customer_id' => 123,
    'billing_info' => [
        'first_name' => 'John',
        'last_name'  => 'Doe',
        'email'      => 'john@example.com',
        'address_1'  => '123 Street',
        'city'       => 'New York',
        'state'      => 'NY',
        'postcode'   => '12345',
        'country'    => 'US'
    ],
    'shipping_info' => [
        // ... shipping details similar to billing
    ],
    'billing_period' => 'month',
    'billing_interval' => 1,
    'start_date' => date('Y-m-d H:i:s'),
    'next_payment_date' => date('Y-m-d H:i:s', strtotime('+1 month')),
    'products' => [
        [
            'product_id' => 456,
            'quantity' => 1,
            'price' => 29.99
        ]
    ]
];

// Create subscription
$creator = new WC_Subscription_Creator();
$subscription = $creator->create_subscription($subscription_data);

if (!is_wp_error($subscription)) {
    // Link payment method
    $creator->link_payment_method(
        wc_get_order($subscription->get_parent_id()),
        $subscription,
        $subscription_data['customer_id']
    );

    // Activate the subscription
    $subscription->update_status('active');
}

Customer Data Preparation

Before creating subscriptions, you’ll need to ensure your customers exist in WooCommerce. If you’re migrating from another platform, you might need to create customers first or match them with existing ones. The most reliable way to match customers is through their email addresses.

function get_or_create_customer($customer_data) {
    // Try to find existing customer by email
    $user = get_user_by('email', $customer_data['email']);
    
    if ($user) {
        return $user->ID;
    }
    
    // Create new customer if not found
    $customer_id = wc_create_new_customer(
        $customer_data['email'],
        $customer_data['email'], // username same as email
        wp_generate_password() // random password
    );
    
    if (is_wp_error($customer_id)) {
        return false;
    }
    
    // Update customer meta
    update_user_meta($customer_id, 'billing_first_name', $customer_data['first_name']);
    update_user_meta($customer_id, 'billing_last_name', $customer_data['last_name']);
    // Add other meta as needed
    
    return $customer_id;
}

Important Note

When implementing this functionality, it’s crucial to ensure your code runs after WooCommerce and WooCommerce Subscriptions are fully initialized. The safest approach is to hook into either `init` or `woocommerce_init` with a relatively high priority:

add_action('init', function() {
    // Your subscription creation code here
}, 999);

Failing to do this might result in unexpected behavior or errors, as the subscription-related functions and classes might not be available when your code runs.

Additional Considerations

While the above code provides the core functionality, here are some additional aspects you might want to consider:

  • Shipping Methods: You can add shipping methods to both the parent order and subscription using WC_Order_Item_Shipping. This is particularly useful if you have specific shipping rules.
  • Error Handling: In a production environment, you’ll want to add proper error handling and perhaps implement a logging system.
  • Batching: If you’re importing a large number of subscriptions, consider implementing a batch processing system to avoid timeout issues.
  • Payment Gateways: While this example uses Authorize.net, the same principles apply to other payment gateways. You’ll just need to adjust the payment token retrieval and meta key names accordingly.

Example: Adding Shipping Methods

Here’s a quick example of how to add shipping methods:

// Add shipping to order/subscription
$shipping_item = new WC_Order_Item_Shipping();
$shipping_item->set_method_title('Standard Shipping');
$shipping_item->set_method_id('flat_rate');
$shipping_item->set_total(10.00);
$order->add_item($shipping_item);

// Don't forget to recalculate totals
$order->calculate_totals();

That’s it! You now have a solid foundation for programmatically creating WooCommerce subscriptions. This approach can be particularly useful when migrating from another platform or when building custom subscription creation flows.

Remember to test thoroughly in a staging environment first, especially when dealing with payment methods, as incorrect setup could lead to failed payments or other issues down the line.