Bonaventure OgetoBy Bonaventure Ogeto|

Insurance Premium Collection with Paystack

At policy enrollment, initialize a zero-naira charge or small tokenization payment to get a reusable authorization_code for the customer card. Store it against the policy. On each premium due date, call POST /transaction/charge_authorization with the stored authorization_code and the premium amount. Handle invoice.payment_failed for dunning, and lapse the policy after 3 failed attempts.

Card Tokenization at Policy Enrollment

When a customer signs up for a policy, collect their card and get a reusable authorization code. Use a small charge (e.g., NGN 50) that you refund immediately, or a zero-value charge if Paystack supports it on your account.

// Step 1: Initialize a tokenization payment
app.post('/api/policy/enroll', async (req, res) => {
  var { email, policyNumber, premium } = req.body;

  var reference = 'enroll_' + policyNumber + '_' + Date.now();

  var response = 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,
      amount: 5000, // NGN 50 tokenization charge
      reference,
      metadata: {
        policy_number: policyNumber,
        action: 'card_tokenization',
      },
    }),
  });

  var data = await response.json();
  res.json({ authorization_url: data.data.authorization_url, reference });
});

// Step 2: After payment, verify and store the authorization_code
app.post('/api/policy/confirm-enrollment', async (req, res) => {
  var { reference } = req.body;

  var verifyRes = await fetch(
    'https://api.paystack.co/transaction/verify/' + reference,
    { headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY } }
  );
  var data = await verifyRes.json();

  if (data.data.status === 'success') {
    var auth = data.data.authorization;
    await db.policies.update({
      authorization_code: auth.authorization_code,
      card_last4: auth.last4,
      card_brand: auth.brand,
      card_reusable: auth.reusable,
    }, { where: { policy_number: data.data.metadata.policy_number } });

    // Optionally refund the NGN 50 tokenization charge
    await refundTransaction(reference);

    res.json({ enrolled: true });
  }
});

Premium Collection Cron Job

// collect-premiums.js — runs daily at 8am
async function collectDuePremiums() {
  var today = new Date().toISOString().split('T')[0];

  var duePolicies = await db.policies.findAll({
    where: {
      status: 'active',
      next_premium_date: today,
      authorization_code: { notNull: true },
    }
  });

  for (var policy of duePolicies) {
    var reference = 'premium_' + policy.policy_number + '_' + Date.now();

    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: policy.customer_email,
        amount: Math.round(policy.premium_amount * 100),
        authorization_code: policy.authorization_code,
        reference,
        metadata: {
          policy_number: policy.policy_number,
          billing_period: today,
        },
      }),
    });

    var data = await response.json();

    if (data.data?.status === 'success') {
      await db.policies.update({
        last_paid_date: today,
        next_premium_date: getNextBillingDate(today, policy.billing_cycle),
        failed_attempts: 0,
      }, { where: { id: policy.id } });
    } else {
      // Failed — dunning will handle via webhook
      console.error('Premium collection failed for', policy.policy_number, data.data?.gateway_response);
    }
  }
}

function getNextBillingDate(from, cycle) {
  var date = new Date(from);
  if (cycle === 'monthly') date.setMonth(date.getMonth() + 1);
  if (cycle === 'quarterly') date.setMonth(date.getMonth() + 3);
  if (cycle === 'annually') date.setFullYear(date.getFullYear() + 1);
  return date.toISOString().split('T')[0];
}

Dunning and Policy Lapse

// Handle failed charge via charge.failed webhook
if (event.event === 'charge.failed') {
  var policyNumber = event.data.metadata?.policy_number;
  if (!policyNumber) return;

  var policy = await db.policies.findByPolicyNumber(policyNumber);
  var failedAttempts = (policy.failed_attempts || 0) + 1;

  if (failedAttempts >= 3) {
    // Lapse the policy
    await db.policies.update({
      status: 'lapsed',
      lapsed_at: new Date().toISOString(),
      failed_attempts: failedAttempts,
    }, { where: { policy_number: policyNumber } });
    await sendPolicyLapseNotification(policy.customer_email, policyNumber);
  } else {
    // Schedule retry: +3 days for attempt 1, +7 days for attempt 2
    var daysUntilRetry = failedAttempts === 1 ? 3 : 7;
    var retryDate = new Date();
    retryDate.setDate(retryDate.getDate() + daysUntilRetry);

    await db.policies.update({
      failed_attempts: failedAttempts,
      next_premium_date: retryDate.toISOString().split('T')[0],
    }, { where: { policy_number: policyNumber } });

    await sendPaymentFailedNotification(policy.customer_email, { failedAttempts, retryDate });
  }
}

Learn More

Key Takeaways

  • Tokenize the card at policy enrollment — do not store card numbers. Paystack gives you a reusable authorization_code.
  • Use POST /transaction/charge_authorization to charge the stored card on each premium due date.
  • Build a scheduled job (cron) that queries due premiums each day and charges them in batches.
  • Handle charge failure with a dunning sequence: retry on day 3, day 7, then lapse the policy.
  • Send email/SMS notifications before the charge date and immediately on failure so customers can update their card.
  • Store the policy_number in the transaction metadata so webhook handlers can match charges to the right policy.

Frequently Asked Questions

Do I need regulatory approval to collect insurance premiums via Paystack?
Yes. In Nigeria, Kenya, and Ghana, insurance premium collection requires a license from the relevant insurance regulatory authority (NAICOM in Nigeria, IRA in Kenya). Paystack handles the payment rails, but your business model must comply with insurance regulation. Check with a licensed insurance partner if you are building insurtech.
What is the difference between Paystack subscriptions and charge_authorization for insurance?
Paystack subscriptions use fixed plan intervals (monthly, quarterly). Insurance premiums may have irregular amounts (renewal, midterm adjustment), custom due dates, or grace periods. Using charge_authorization directly gives you full control over timing and amount — recommended for insurance use cases.
How do I handle a customer who wants to update their card?
Send the customer a link to re-enroll: initialize a new tokenization transaction. When they complete it, update the authorization_code in your database. Old charges continue until the new code is stored. Do not delete the old code until the new one is confirmed as reusable.
Can I charge a customer before the premium due date if they have insufficient balance?
No. You cannot pre-charge a card without customer consent for a specific billing period. Send a reminder 3-5 days before the due date and advise them to ensure sufficient card balance. Some insurers send a test authorization a day before the due date, but this requires explicit consent in your terms.

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