Guide
Build a billing page
Show a customer what they owe, what they've paid, and let them download and pay invoices, from your own product.
Scopes: customers:read and invoices:read. Add payments:write only if you start checkouts yourself.
Totals#
GET /customers/{id}/statement gives open, overdue and paid totals and counts, per currency. Show one block per currency; never add NGN and USD together.
The invoice list#
GET /invoices?customerId= lists the customer's invoices, newest first. Each has status, overdue, total, dueDate and display-ready lines.
| status | Show the customer | Buttons |
|---|---|---|
| awaiting_approval awaiting_final_approval returned | Nothing: they haven't been sent it yet. | None |
| approved | “Due”, or “Overdue” when overdue is true. | Download, Pay |
| paid | “Paid” on paidAt. | Download invoice, Download receipt |
| voided | “Cancelled”, or hide it. | None |
Download and Pay buttons#
- Call
POST /invoices/{id}/linksfor a freshpdfUrlandpayUrlwhen the customer needs them. Links expire, so don't store them. - With a few invoices on the page, fetch links as it loads. With many, link each button to your server and fetch the link on click, as below. That also keeps you well under the rate limit.
payUrlisnullfor paid and voided invoices: hide Pay.- For a receipt, use
POST /receipts/{invoiceId}/links.
// Express. Your page calls these routes on your server; the API key never reaches the browser.
const flux = (path, init = {}) =>
fetch(`${process.env.FLUX_BASE_URL}/api/v1${path}`, {
...init,
headers: { Authorization: `ApiKey ${process.env.FLUX_API_KEY}`, ...init.headers },
});
app.get("/billing", requireLogin, async (req, res) => {
const customerId = req.user.fluxCustomerId;
const [statement, invoices] = await Promise.all([
flux(`/customers/${customerId}/statement`).then((r) => r.json()),
flux(`/invoices?customerId=${customerId}&limit=50`).then((r) => r.json()),
]);
// Show only what the customer has been sent: approved, paid (and voided, if you like).
const visible = invoices.data.filter((i) => ["approved", "paid"].includes(i.status));
res.render("billing", { totals: statement.currencies, invoices: visible });
});
// "Download" and "Pay" buttons link here. Fresh links on every click; nothing stored.
app.get("/billing/invoices/:id/:action(pdf|pay)", requireLogin, async (req, res) => {
const invoice = await flux(`/invoices/${req.params.id}`).then((r) => r.json());
if (invoice.customerId !== req.user.fluxCustomerId) return res.sendStatus(404);
const links = await flux(`/invoices/${invoice.id}/links`, { method: "POST" }).then((r) => r.json());
const target = req.params.action === "pay" ? links.payUrl : links.pdfUrl;
return target ? res.redirect(target) : res.redirect("/billing");
});# Flask. Your page calls these routes on your server; the API key never reaches the browser.
import os
import requests
from flask import abort, redirect, render_template
BASE = f"{os.environ['FLUX_BASE_URL']}/api/v1"
HEADERS = {"Authorization": f"ApiKey {os.environ['FLUX_API_KEY']}"}
def flux(method, path, **kwargs):
return requests.request(method, BASE + path, headers=HEADERS, timeout=30, **kwargs).json()
@app.get("/billing")
@login_required
def billing():
customer_id = current_user.flux_customer_id
statement = flux("GET", f"/customers/{customer_id}/statement")
invoices = flux("GET", "/invoices", params={"customerId": customer_id, "limit": 50})
# Show only what the customer has been sent: approved, paid (and voided, if you like).
visible = [i for i in invoices["data"] if i["status"] in ("approved", "paid")]
return render_template("billing.html", totals=statement["currencies"], invoices=visible)
# "Download" and "Pay" buttons link here. Fresh links on every click; nothing stored.
@app.get("/billing/invoices/<int:invoice_id>/<any(pdf, pay):action>")
@login_required
def invoice_link(invoice_id, action):
invoice = flux("GET", f"/invoices/{invoice_id}")
if invoice.get("customerId") != current_user.flux_customer_id:
abort(404)
links = flux("POST", f"/invoices/{invoice_id}/links")
target = links["payUrl"] if action == "pay" else links["pdfUrl"]
return redirect(target or "/billing")Keep the key on the server
Call the API from your server. Check the invoice belongs to the signed-in customer before you return links for it.
Keeping it fresh#
Read from Flux when the page loads, or cache briefly and refresh on invoice.issued, invoice.paid and invoice.voided webhooks.