Guide

Act on billing cycles

Flux knows when each enrollment's billing cycle comes round, but it never invoices on its own. Use enrollment.cycle_due to raise the next invoice from your system.

Scope: invoices:write, and a webhook endpoint subscribed to enrollment.cycle_due.

Needs invoice reminders

enrollment.cycle_due fires only for organizations that have turned on invoice reminders in Flux (off by default). Ask the organization's admin to turn them on. Without them, run your own schedule from your billing data.

The event#

When an enrollment's billing date arrives, Flux sends enrollment.cycle_due with the enrollment object: customerId, serviceId, amount, billingCycle, cycleDate (the date that arrived) and nextBillingDate. Flux doesn't raise an invoice for it. That's your decision.

Raise the invoice#

Use the enrollment id and cycleDate in externalRef, so a duplicate event can't raise a second invoice.

const MONTHS = { monthly: 1, quarterly: 3, half_yearly: 6, yearly: 12 };
const DAY = 86400000;
const day = (d) => d.toISOString().slice(0, 10);

// Dates arrive as "2026-11-01T00:00:00". Read them as UTC dates, not local times.
const utcDate = (s) => new Date(s.slice(0, 10) + "T00:00:00Z");

function addMonths(d, n) {
  const r = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + n, 1));
  const lastDay = new Date(Date.UTC(r.getUTCFullYear(), r.getUTCMonth() + 1, 0)).getUTCDate();
  r.setUTCDate(Math.min(d.getUTCDate(), lastDay));
  return r;
}

async function onCycleDue(event) {
  const e = event.data.object;                              // the enrollment
  const start = utcDate(e.cycleDate);
  const end = new Date(addMonths(start, MONTHS[e.billingCycle] ?? 1).getTime() - DAY);

  await fetch(`${process.env.FLUX_BASE_URL}/api/v1/invoices`, {
    method: "POST",
    headers: {
      Authorization: `ApiKey ${process.env.FLUX_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `cycle-${e.id}-${day(start)}`,
    },
    body: JSON.stringify({
      externalRef: `enr_${e.id}-${day(start)}`,            // one invoice per enrollment per cycle
      customerId: e.customerId,
      currency: "NGN",
      dueDate: day(new Date(start.getTime() + 14 * DAY)),
      periodStart: day(start),
      periodEnd: day(end),
      nextDueDate: e.nextBillingDate?.slice(0, 10),
      invoiceType: "subscription",
      lines: [{ serviceId: e.serviceId, amount: e.amount }],
    }),
  });
}

Set the currency and due date by your own rules: the example uses NGN and 14 days. If you bill by workspace and period instead, use the reference scheme from Bill without double billing, so either path finds the other's invoice.

Related events#

  • enrollment.started: a customer was enrolled. A good time to raise a setup invoice.
  • enrollment.ended: stop raising invoices for it.