Bonaventure OgetoBy Bonaventure Ogeto|

Build an Insurance Premium Collection Portal

Build an insurance premium portal by creating a Paystack Plan per insurance product (health, motor, microinsurance), subscribing policyholders via the transaction initialize endpoint, setting policy status to "active" on subscription.create, applying a 14-day grace period on invoice.payment_failed before suspending coverage, and disabling the subscription on subscription.disable to mark the policy as "lapsed".

Policy Enrollment and Premium Setup

Tables: insurance_products (name, type, coverage_amount, premium_monthly, paystack_plan_code, terms_url), policies (holder_id, product_id, policy_number, paystack_subscription_code, status, start_date, grace_until, expires_at), claims (policy_id, type, amount, status, submitted_at).

function generatePolicyNumber() {
  return 'POL-' + new Date().getFullYear() + '-' + Math.random().toString(36).toUpperCase().slice(2, 8);
}

async function enrollPolicyholder(holderEmail, holderId, productId) {
  var product = await db.insuranceProducts.findById(productId);

  var policy = await db.policies.create({
    holder_id: holderId,
    product_id: productId,
    policy_number: generatePolicyNumber(),
    status: 'pending_first_payment',
    start_date: new Date(),
  });

  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: holderEmail,
      amount: product.premium_monthly,
      plan: product.paystack_plan_code,
      metadata: { holder_id: holderId, policy_id: policy.id, product_id: productId },
    }),
  });
  return { policy, authorizationUrl: (await res.json()).data.authorization_url };
}

// Webhook: activate policy on first premium
if (event.event === 'subscription.create') {
  var meta = event.data.metadata;
  var nextPayment = new Date(event.data.next_payment_date);

  await db.policies.update(meta.policy_id, {
    paystack_subscription_code: event.data.subscription_code,
    status: 'active',
    expires_at: nextPayment,
  });
  await issuePolicyCertificate(meta.policy_id, meta.holder_id);
}

Premium Renewal, Grace Period, and Policy Lapse

// Renewal confirmed
if (event.event === 'invoice.create' && event.data.status === 'success') {
  var code = event.data.subscription.subscription_code;
  await db.policies.update({ code }, {
    status: 'active',
    grace_until: null,
    expires_at: new Date(event.data.next_payment_date),
  });
}

// Failed payment: enter grace period
if (event.event === 'invoice.payment_failed') {
  var code = event.data.subscription.subscription_code;
  var graceUntil = new Date();
  graceUntil.setDate(graceUntil.getDate() + 14); // 14-day grace period

  await db.policies.update({ code }, { status: 'grace_period', grace_until: graceUntil });
  await sendPremiumFailedEmail(code,
    'Your premium payment failed. Coverage continues until ' + graceUntil.toDateString() +
    '. Please update your payment method to avoid policy lapse.'
  );
}

// Policy cancelled or subscription disabled — lapse
if (event.event === 'subscription.disable') {
  var code = event.data.subscription_code;
  await db.policies.update({ code }, { status: 'lapsed' });
  await notifyPolicyLapsed(code);
}

// Cron: suspend coverage after grace period expires
async function suspendLapsedPolicies() {
  var now = new Date();
  var overdue = await db.policies.findAll({ status: 'grace_period', grace_until_lt: now });
  for (var policy of overdue) {
    await db.policies.update(policy.id, { status: 'suspended' });
    await sendCoverageSuspendedEmail(policy.holder_id);
  }
}

// Claim eligibility check
async function isEligibleForClaim(policyId) {
  var policy = await db.policies.findById(policyId);
  return policy.status === 'active' || policy.status === 'grace_period';
}

Learn More

See build a gym membership app for the same grace period and subscription lifecycle pattern applied to a simpler membership product.

Sign up for the McTaba newsletter

Key Takeaways

  • Create one Paystack Plan per insurance product — link each policy to the correct plan code.
  • Map subscription lifecycle events to policy status: active, grace_period, suspended, lapsed.
  • Apply a 14-day grace period on invoice.payment_failed before suspending coverage.
  • Generate a digital policy certificate on the first premium payment confirmation.
  • For microinsurance, consider weekly or daily Paystack Plans to match informal worker income patterns.

Frequently Asked Questions

Do I need an insurance licence to collect premiums via Paystack?
Yes. In Nigeria, collecting insurance premiums requires NAICOM licensing. In Kenya, you need IRA approval. Building a premium collection portal for a licensed insurer (as a technology partner, not the insurer itself) is the typical developer role. Your platform collects payments on behalf of the licensed insurer, which holds the risk and regulatory obligations.
How should I handle a claim submission?
Claims are separate from payments. Build a claims portal where policyholders upload supporting documents (hospital bills, police reports, photos), and a claims assessor reviews and approves or rejects. Payment of approved claims goes via Paystack Transfers to the policyholder's bank account. Track claim processing time as a key service metric.
Can I offer microinsurance with weekly premiums instead of monthly?
Yes. Paystack Plans support weekly billing intervals. Create a weekly_premium plan with a lower amount (e.g., NGN 250/week = NGN 1,000/month equivalent). Weekly billing matches the income patterns of informal workers who are paid daily or weekly. The subscription lifecycle webhooks work identically — only the invoice frequency changes.

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