Bonaventure OgetoBy Bonaventure Ogeto|

Rent Collection with Paystack Recurring Debits

At lease signing, tokenize the tenant card with a small authorization charge. Store the authorization_code against the tenancy record. On the first of each month, run a cron job that calls charge_authorization for each active tenancy. On success, trigger a Paystack transfer to the landlord recipient. On failure, notify the tenant and set a grace period before flagging as overdue.

Tenancy Database Schema

CREATE TABLE tenancies (
  id SERIAL PRIMARY KEY,
  tenant_email TEXT NOT NULL,
  tenant_name TEXT NOT NULL,
  authorization_code TEXT,     -- Paystack reusable auth
  card_last4 TEXT,
  property_id INTEGER NOT NULL,
  landlord_recipient_code TEXT NOT NULL, -- Paystack transfer recipient
  rent_amount NUMERIC(12, 2) NOT NULL,   -- Monthly rent in local currency
  service_charge NUMERIC(12, 2) DEFAULT 0,
  currency TEXT DEFAULT 'NGN',
  rent_due_day INTEGER DEFAULT 1,        -- Day of month
  lease_start DATE NOT NULL,
  lease_end DATE NOT NULL,
  status TEXT DEFAULT 'active',          -- active | overdue | terminated
  last_paid_date DATE,
  failed_attempts INTEGER DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Monthly Rent Collection Job

// collect-rent.js — run on the 1st of each month (or configured due day)
async function collectMonthlyRent() {
  var today = new Date();
  var currentDay = today.getDate();

  var dueTenancies = await db.tenancies.findAll({
    where: {
      status: 'active',
      rent_due_day: currentDay,
      authorization_code: { notNull: true },
      lease_end: { gt: today },
    }
  });

  for (var tenancy of dueTenancies) {
    var reference = 'rent_' + tenancy.id + '_' + today.toISOString().slice(0, 7);

    // Check not already collected this month
    var alreadyPaid = await db.payments.findOne({
      where: { tenancy_id: tenancy.id, reference }
    });
    if (alreadyPaid) continue;

    var totalCharge = tenancy.rent_amount + tenancy.service_charge;

    var response = await fetch('https://api.paystack.co/transaction/charge_authorization', {
      method: 'POST',
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: tenancy.tenant_email,
        amount: Math.round(totalCharge * 100),
        authorization_code: tenancy.authorization_code,
        reference,
        currency: tenancy.currency,
        metadata: { tenancy_id: tenancy.id, type: 'rent' },
      }),
    });

    var data = await response.json();

    if (data.data?.status === 'success') {
      await db.payments.create({ tenancy_id: tenancy.id, reference, amount: totalCharge, status: 'paid' });
      await db.tenancies.update({ last_paid_date: today, failed_attempts: 0 }, { where: { id: tenancy.id } });

      // Pay landlord (minus service charge)
      await initiateTransfer(tenancy.landlord_recipient_code, tenancy.rent_amount, tenancy.currency);
    }
  }
}

async function initiateTransfer(recipientCode, amount, currency) {
  return fetch('https://api.paystack.co/transfer', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      source: 'balance',
      recipient: recipientCode,
      amount: Math.round(amount * 100),
      currency,
      reason: 'Monthly rent payment',
    }),
  }).then(r => r.json());
}

Learn More

Key Takeaways

  • Tokenize tenant card at lease signing — you need a reusable authorization_code to charge each month.
  • Run a monthly cron job on the rent due date. Use charge_authorization, not a Paystack subscription plan.
  • On successful rent charge, immediately initiate a Paystack transfer to the landlord recipient.
  • Keep service charge and management fees from the rent amount before transferring to the landlord.
  • Give tenants a 5-day grace period before marking rent as overdue and sending late payment notices.
  • Store tenancy details in a database: tenant_email, authorization_code, rent_amount, landlord_recipient_code, due_day.

Frequently Asked Questions

Can I charge different amounts each month for rent?
Yes. charge_authorization lets you specify the amount on each call. Unlike Paystack subscription plans which have a fixed amount, charge_authorization is fully flexible. This is useful for rent that varies with utilities, or where the first month is prorated.
What if a tenant disputes the automatic rent charge?
They can raise a dispute with their bank (chargeback). Your strongest defence is a signed lease that authorizes recurring debit, a clear record of when and how much was charged, and a reference tied to a specific billing month. Keep all of this in your database and accessible for dispute resolution.
How do I handle a tenant who wants to change their debit card?
Send them a card update link — initialize a new tokenization charge, verify it, get the new authorization_code, and update the tenancy record. Keep the old code until the new one is confirmed. Never allow tenants to update their own authorization_code directly via your API without authentication.
Should I use Paystack transfers or manual bank transfer to pay landlords?
Paystack transfers (programmatic) are strongly preferred. They are instant, auditable, and trigger a transfer.success webhook. Manual bank transfers require you to log into a banking portal and are error-prone. Create a Paystack recipient for each landlord once and store their recipient_code for all future payouts.

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