Core concepts
Payments
Customers pay approved invoices online with Paystack. You start the payment; Flux verifies it with Paystack and tells you by webhook.
- You
Start the payment
POST /invoices/{id}/checkoutreturns a Paystack URL. Or give the payer the signedpayUrl. - Payer
Pays on Paystack
By card, transfer or any channel Paystack offers. Paystack redirects them back.
- Flux
Verifies
Flux checks the payment with Paystack: the money must cover the invoice total, in its currency.
- Flux → you
Tells you
payment.succeeded,invoice.paidandreceipt.issued. Orpayment.failed.
Online and offline invoices#
When an invoice is approved, the organization decides how it's paid: online (Paystack) or offline (bank transfer). Only online invoices can be paid through the API or a payUrl. Starting a checkout for an offline invoice gets 409 not_online.
Starting a payment#
There are two ways. Both reach the same Paystack checkout.
- Checkout: call
POST /invoices/{id}/checkout(scopepayments:write) and redirect the payer tocheckoutUrl. - Pay link: give the payer the signed
payUrlfromPOST /invoices/{id}/links. Opening it starts the checkout. No payment scope is needed. See Signed links.
curl -X POST "$FLUX_BASE_URL/api/v1/invoices/5012/checkout" \
-H "Authorization: ApiKey $FLUX_API_KEY"// Node.js 18+ (built-in fetch). Save as an .mjs file.
const response = await fetch(`${process.env.FLUX_BASE_URL}/api/v1/invoices/5012/checkout`, {
method: "POST",
headers: {
Authorization: `ApiKey ${process.env.FLUX_API_KEY}`,
},
});
console.log(response.status, await response.json());import os
import requests
response = requests.post(
f"{os.environ['FLUX_BASE_URL']}/api/v1/invoices/5012/checkout",
headers={"Authorization": f"ApiKey {os.environ['FLUX_API_KEY']}"},
timeout=30,
)
print(response.status_code, response.json())<?php
$ch = curl_init(getenv('FLUX_BASE_URL') . '/api/v1/invoices/5012/checkout');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: ApiKey ' . getenv('FLUX_API_KEY'),
],
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), ' ', $response, PHP_EOL;using System.Net.Http.Headers;
var baseUrl = Environment.GetEnvironmentVariable("FLUX_BASE_URL");
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("ApiKey", Environment.GetEnvironmentVariable("FLUX_API_KEY"));
using var request = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/api/v1/invoices/5012/checkout");
using var response = await http.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("POST", os.Getenv("FLUX_BASE_URL")+"/api/v1/invoices/5012/checkout", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "ApiKey "+os.Getenv("FLUX_API_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode, string(out))
}200 OK
{
"checkoutUrl": "https://checkout.paystack.com/7xk2m9q4fz0b1ac"
}| Error | Meaning |
|---|---|
409 invoice_voided | The invoice is voided. |
409 already_paid | The invoice is already paid. |
409 not_online | The invoice wasn't approved for online payment. |
409 payment_not_available | Online payment can't start, for example because the organization's payment settings aren't complete. The message explains. |
502 gateway_error | Paystack refused to start the payment. details holds its response. Retry later. |
Confirmation is asynchronous#
Paystack redirecting the payer back to you isn't proof of payment. Show a “processing” state until Flux confirms it.
- Flux verifies every payment with Paystack itself.
- An invoice is marked paid only when the amount paid covers the invoice total in the invoice's currency. Paying more, for example when the payer covers gateway fees, is accepted.
- A short payment, or one in another currency, isn't applied. The invoice stays unpaid, and Flux sends
payment.failed(withproblemsaying why) andinvoice.payment_review. The organization sorts it out in Flux. - Listen for
payment.succeeded,invoice.paidorreceipt.issued. If you can't receive webhooks, pollGET /invoices/{id}untilstatusispaid.
| Outcome | Events, in the order Flux emits them | Invoice |
|---|---|---|
| Checkout started | payment.initiated | unchanged |
| Paid in full | payment.succeeded, invoice.paid, receipt.issued | paid |
| Short, or wrong currency | payment.failed (with problem), invoice.payment_review | unchanged, paymentStatus explains |
| Declined by Paystack | payment.failed (problem is null) | unchanged |
| Refunded in Paystack | payment.refunded | still paid; paymentStatus notes the refund |
Events can arrive in a different order than they were emitted. Re-read the invoice when order matters.
Fees#
- The payer bears Paystack's fee.
- When the organization collects through Flux's Paystack account: Flux adds a platform fee to the amount the payer pays: a flat amount plus a percentage, capped. It's shown as
platformFeeMinoronpayment.initiated. whether to publish the fee figures - Organizations on their own Paystack account pay no platform fee.
Refunds#
Refunds are made in Paystack. Flux records them and sends payment.refunded, with the amount in minor units. Whether to reverse the invoice is an accounting decision taken in Flux, not by the API.