Guide

Take payment and confirm it

Send the customer to Paystack, show a processing state when they come back, and mark the invoice paid only when Flux says so.

Scopes: payments:write to start checkouts (or none, if you use payUrl), and a webhook endpoint for confirmation.

1. Start the payment#

Show Pay only for approved invoices that aren't paid. Then either start a checkout with POST /invoices/{id}/checkout and redirect to checkoutUrl, or link to a fresh payUrl. Handle the 409 codes with a clear message: “already paid”, “cancelled”, “pay by bank transfer”.

2. When the payer comes back#

Paystack sends the payer back after paying. That redirect isn't proof of payment. Show “Payment processing. We'll confirm shortly.” and wait for Flux.

3. Confirm from Flux#

  • Webhooks (recommended): invoice.paid or receipt.issued means it's paid. payment.succeeded comes with them.
  • Polling, if you can't receive webhooks: GET /invoices/{id} every 30 to 60 seconds for a few minutes, until status is paid. Then less often.
// 1. Pay button -> your server starts the checkout and redirects.
app.post("/billing/invoices/:id/pay", requireLogin, async (req, res) => {
  const r = await flux(`/invoices/${req.params.id}/checkout`, { method: "POST" });
  if (r.ok) return res.redirect(303, (await r.json()).checkoutUrl);
  const error = await r.json();
  // already_paid, invoice_voided, not_online, payment_not_available, gateway_error
  return res.redirect(`/billing?notice=${encodeURIComponent(error.code)}`);
});

// 2. Back from Paystack: show "processing" until Flux confirms. Never mark it paid here.

// 3. Webhook handler (after verifying the signature and deduplicating):
async function onFluxEvent(event) {
  const obj = event.data.object;
  switch (event.type) {
    case "invoice.paid":
      await markPaid(obj.id, obj.paidAt);
      break;
    case "invoice.payment_review":
      await flagForReview(obj.id, obj.paymentStatus);    // short payment or wrong currency
      break;
    case "payment.failed":
      await notePaymentFailed(obj.invoiceId, obj.problem); // problem is null when Paystack declined
      break;
  }
}

4. When it doesn't work out#

  • invoice.payment_review: the money didn't cover the invoice, or came in another currency. The invoice stays unpaid and the organization deals with it in Flux. Tell the customer their payment is being reviewed; don't ask them to pay again.
  • payment.failed with problem: null: Paystack declined the payment. The customer can try again.
  • Refunds are made in Paystack and arrive as payment.refunded.