Bonaventure OgetoBy Bonaventure Ogeto|

Build a Subscription Box Business Backend

Build a subscription box backend using Paystack Plans for monthly billing. Subscribers sign up via the transaction initialize endpoint with a plan attached. Your backend receives subscription events, manages box assignments for each cycle, handles skips and pauses, and triggers shipping when a cycle payment succeeds.

Plans and Subscriber Sign-Up

Tables: plans (name, description, price, paystack_plan_code), subscribers (name, email, phone, address, plan_id, paystack_subscription_code, status, joined_at), box_cycles (month, year, cutoff_date), shipments (subscriber_id, cycle_id, box_contents, shipping_address, status, tracking_number).

// Create plans once (run during setup)
async function createSubscriptionPlan(name, priceInKobo, description) {
  var res = await fetch('https://api.paystack.co/plan', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ name, amount: priceInKobo, interval: 'monthly', currency: 'NGN', description }),
  });
  return (await res.json()).data.plan_code;
}

// Subscriber sign-up
async function subscribeCustomer(customerEmail, planId) {
  var plan = await db.plans.findById(planId);
  var res = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: customerEmail, amount: plan.price,
      plan: plan.paystack_plan_code,
      metadata: { plan_id: planId, purpose: 'subscription_signup' },
    }),
  });
  return (await res.json()).data.authorization_url;
}

// Webhook: subscription created
if (event.event === 'subscription.create') {
  var meta = event.data.metadata;
  await db.subscribers.update({ email: event.data.customer.email }, {
    paystack_subscription_code: event.data.subscription_code,
    status: 'active',
  });
}

Billing Cycles and Box Shipping

// Each month's renewal triggers box preparation
if (event.event === 'invoice.create' && event.data.status === 'success') {
  var subscriptionCode = event.data.subscription.subscription_code;
  var subscriber = await db.subscribers.findByCode(subscriptionCode);
  var currentCycle = await db.boxCycles.findCurrentMonth();

  // Only prepare box if subscriber joined before cutoff
  if (subscriber.joined_at <= currentCycle.cutoff_date) {
    await db.shipments.create({
      subscriber_id: subscriber.id,
      cycle_id: currentCycle.id,
      shipping_address: subscriber.address,
      status: 'preparing',
    });
    await notifyWarehouse(subscriber.id, currentCycle.id);
  }
}

// Handle failed payment — grace period
if (event.event === 'invoice.payment_failed') {
  var code = event.data.subscription.subscription_code;
  await db.subscribers.update({ subscription_code: code }, { status: 'payment_failed' });
  await sendRetryPaymentEmail(code); // remind to update payment method
}

// Skip a month — subscriber requests via your website
async function skipMonth(subscriberId, skipReason) {
  var subscriber = await db.subscribers.findById(subscriberId);
  // Disable next renewal
  await fetch('https://api.paystack.co/subscription/disable', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ code: subscriber.paystack_subscription_code, token: subscriber.email_token }),
  });
  await db.subscribers.update(subscriberId, { next_action: 'skip', skip_reason: skipReason });
}

Learn More

See build a water delivery subscription service for a recurring delivery model with similar billing architecture.

Sign up for the McTaba newsletter

Key Takeaways

  • Use Paystack Plans for recurring monthly billing — one plan per box tier.
  • Each successful billing cycle (invoice.success) triggers box preparation and shipping.
  • Allow subscribers to skip a month: disable renewal for that cycle via Paystack API.
  • Handle invoice.payment_failed with a grace period before suspending the subscription.
  • Cutoff dates matter: decide by which day of the month subscriptions need to activate to receive that month's box.

Frequently Asked Questions

What is the cutoff date and why does it matter?
The cutoff date is the last day a customer can subscribe and still receive that month's box. If you ship on the 15th of each month, your cutoff might be the 5th. Subscribers who join after the 5th get their first box next month. Communicate this clearly during signup so customers know when to expect their first delivery.
How do I manage box contents for each subscriber?
Create a box curation system for each monthly cycle. You define the box contents (products, quantities) in a cycle plan. When a shipment is created for a subscriber, it inherits the current cycle's box contents. Customization options (dietary preferences, size preferences) can filter or swap items from the base box.
How do I let subscribers cancel their subscription?
POST to the Paystack disable/cancel subscription endpoint with the subscriber's subscription code. Update your subscriber record to status "cancelled". Give the subscriber until the end of the current billing period before access/shipping stops — they have paid for the current month, do not cut them off mid-cycle.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon