Core concepts
Errors
Every v1 error has the same body. Branch your code on the code, and show the message to an operator if you like.
The error body#
{
"code": "invalid_request",
"message": "Add at least one invoice item",
"details": null
}codeis stable and machine-readable. Branch on it, never onmessage.messageis plain English and safe to show to an operator. Its wording can change.detailscarries extra data for some codes, elsenull.
Error codes#
| HTTP | code | Meaning | Retry? |
|---|---|---|---|
400 | invalid_request | Validation failed. The message says what, such as Add at least one invoice item. | No. Fix the request. |
400 | invalid_idempotency_key | Idempotency-Key is longer than 255 characters. | No. Use a shorter key. |
401 | unauthorized | No API key, an unknown key or a revoked key. | No. Fix or replace the key. |
403 | insufficient_scope | The key lacks the scope. details names it: { "required": "invoices:write" }. | No. Ask the admin for a key with the scope. |
404 | not_found | Unknown id, or an id that belongs to another organization. | No. |
404 | not_paid | You asked for the receipt of an unpaid invoice. | Later, after invoice.paid. |
409 | conflict | A uniqueness rule failed, such as another customer having the same externalRef. | No. Look up the existing record. |
409 | idempotency_in_progress | A request with the same Idempotency-Key is still running. | Yes, after a short wait, with the same key. |
409 | invoice_voided | Checkout: the invoice is voided. | No. |
409 | already_paid | Checkout: the invoice is already paid. | No. |
409 | not_online | Checkout: the invoice wasn't approved for online payment. | No. The organization decides how the invoice is paid. |
409 | payment_not_available | Checkout: online payment can't start, for example because the organization's payment settings aren't complete. The message explains. | Not until the organization fixes its payment settings. |
422 | idempotency_key_reused | The same Idempotency-Key was sent with a different request. | No. Use a new key for a different request. |
429 | rate_limited | Too many requests for this key. Wait for the Retry-After seconds. | Yes, after Retry-After. |
502 | gateway_error | Paystack didn't start the payment. details holds the gateway's response. | Yes, with backoff. |
503 | links_not_configured | The server has no link-signing key configured. | No. Contact Flux. |
Validation messages#
400 invalid_request covers every validation rule. The message says which one failed. Some you may see:
Add at least one invoice itemcurrency must be a valid ISO code, e.g. NGNOne or more invoice items reference an unknown serviceDiscount amount cannot be higher than invoice amountEnter both the billing period start and endInvalid customerInvalid email addressFirst name and last name are required for an individual customerCompany name is required for a company customerA customer can have at most 20 billing contactsOnly one billing contact can be primaryWebhook URLs must use httpsUnknown event type 'invoice.sent'
Retrying#
- Retry
429after theRetry-Afterseconds, and5xxresponses, timeouts and network errors with exponential backoff and jitter. - Don't retry other
4xxresponses unchanged. They fail the same way every time. - Retry creates with the same
Idempotency-Key. Flux then either runs the request once or replays the first response. See Idempotency.
// Retries 429 (after Retry-After) and 5xx/network errors. Keep the same Idempotency-Key on retries.
async function fluxFetch(url, options = {}, attempts = 5) {
for (let attempt = 1; ; attempt++) {
let response;
try {
response = await fetch(url, options);
} catch (networkError) {
if (attempt >= attempts) throw networkError;
}
if (response && response.status < 500 && response.status !== 429) return response;
if (attempt >= attempts) return response;
const retryAfter = Number(response?.headers.get("Retry-After"));
const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(30000, 500 * 2 ** attempt) * (0.5 + Math.random());
await new Promise((resolve) => setTimeout(resolve, delay));
}
}# Retries 429 (after Retry-After) and 5xx/network errors. Keep the same Idempotency-Key on retries.
import random
import time
import requests
def flux_request(method, url, attempts=5, **kwargs):
for attempt in range(1, attempts + 1):
try:
response = requests.request(method, url, timeout=30, **kwargs)
except requests.RequestException:
if attempt == attempts:
raise
response = None
if response is not None and response.status_code < 500 and response.status_code != 429:
return response
if attempt == attempts:
return response
retry_after = int(response.headers.get("Retry-After", 0)) if response is not None else 0
time.sleep(retry_after or min(30, 0.5 * 2 ** attempt) * (0.5 + random.random()))// Retries 429 (after Retry-After) and 5xx/network errors. Keep the same Idempotency-Key on retries.
static async Task<HttpResponseMessage> SendWithRetry(HttpClient http, Func<HttpRequestMessage> build, int attempts = 5)
{
for (var attempt = 1; ; attempt++)
{
HttpResponseMessage? response = null;
try { response = await http.SendAsync(build()); }
catch (HttpRequestException) when (attempt < attempts) { }
if (response != null && (int)response.StatusCode < 500 && (int)response.StatusCode != 429) return response;
if (attempt >= attempts) return response!;
var delay = response?.Headers.RetryAfter?.Delta
?? TimeSpan.FromMilliseconds(Math.Min(30000, 500 * Math.Pow(2, attempt)) * (0.5 + Random.Shared.NextDouble()));
await Task.Delay(delay);
}
}