Core concepts
Idempotency
Networks fail. Idempotency lets you retry a create without creating it twice: an Idempotency-Key for any create, and externalRef for invoices, which never double-bills.
Idempotency-Key#
Send Idempotency-Key: <unique string> on these requests:
POST /customersPOST /invoicesPOST /webhook-endpoints
Keys are at most 255 characters (longer gets 400 invalid_idempotency_key). Flux keeps each key for 24 hours, separately for each API key.
curl -X POST "$FLUX_BASE_URL/api/v1/customers" \
-H "Authorization: ApiKey $FLUX_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-customer-ws_42" \
-d '{
"customerType": "company",
"companyName": "Leadway Group",
"email": "finance@leadway.example",
"externalRef": "ws_42"
}'// Node.js 18+ (built-in fetch). Save as an .mjs file.
const response = await fetch(`${process.env.FLUX_BASE_URL}/api/v1/customers`, {
method: "POST",
headers: {
Authorization: `ApiKey ${process.env.FLUX_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "create-customer-ws_42",
},
body: JSON.stringify({
customerType: "company",
companyName: "Leadway Group",
email: "finance@leadway.example",
externalRef: "ws_42",
}),
});
console.log(response.status, await response.json());import os
import requests
response = requests.post(
f"{os.environ['FLUX_BASE_URL']}/api/v1/customers",
headers={
"Authorization": f"ApiKey {os.environ['FLUX_API_KEY']}",
"Idempotency-Key": "create-customer-ws_42",
},
json={
"customerType": "company",
"companyName": "Leadway Group",
"email": "finance@leadway.example",
"externalRef": "ws_42",
},
timeout=30,
)
print(response.status_code, response.json())<?php
$body = <<<'JSON'
{
"customerType": "company",
"companyName": "Leadway Group",
"email": "finance@leadway.example",
"externalRef": "ws_42"
}
JSON;
$ch = curl_init(getenv('FLUX_BASE_URL') . '/api/v1/customers');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: ApiKey ' . getenv('FLUX_API_KEY'),
'Content-Type: application/json',
'Idempotency-Key: create-customer-ws_42',
],
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), ' ', $response, PHP_EOL;using System.Net.Http.Headers;
using System.Text;
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/customers");
request.Headers.Add("Idempotency-Key", "create-customer-ws_42");
request.Content = new StringContent("""
{
"customerType": "company",
"companyName": "Leadway Group",
"email": "finance@leadway.example",
"externalRef": "ws_42"
}
""", Encoding.UTF8, "application/json");
using var response = await http.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
body := strings.NewReader(`{
"customerType": "company",
"companyName": "Leadway Group",
"email": "finance@leadway.example",
"externalRef": "ws_42"
}`)
req, err := http.NewRequest("POST", os.Getenv("FLUX_BASE_URL")+"/api/v1/customers", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "ApiKey "+os.Getenv("FLUX_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "create-customer-ws_42")
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))
}How Flux treats a repeated key#
| When you send a key that was used before | Flux |
|---|---|
| With the same request (method, path and body) | Replays the first response, status and body, with the header Idempotent-Replayed: true. Nothing runs again. |
| With a different request | Refuses it: 422 idempotency_key_reused. |
| While the first request is still running | Refuses it: 409 idempotency_in_progress. Wait a moment and retry. |
| After the first request failed with a 5xx or an exception | Runs it: the key wasn't kept. Retrying with the same key is safe. |
| After the first request failed with a 4xx | Replays that 4xx. Fix the request and send it with a new key. |
Choosing keys#
- Derive the key from your own ids, such as
create-customer-ws_42, or generate a UUID and save it before sending, so a retry after a crash uses the same key. - Use a new key for each distinct operation. Reusing a key for a different body is refused.
- Other POSTs (links, checkout, test, redeliver, rotate-secret) don't take a key. Links and checkout are safe to repeat. Rotating twice gives two new secrets, and only the last one works.
externalRef: never double-bill#
POST /invoices is also idempotent on externalRef, permanently, not just for 24 hours:
- If no invoice in the organization has that
externalRef, Flux raises one and returns201. - If one exists, Flux returns it with
200, in its current state, and raises nothing. This holds even for two requests that arrive at the same moment.
Use a stable id for what you're billing: your customer id and the billing period, such as ws_42-2026-11. This is the recommended way to avoid double billing. See Bill without double billing.
A customer's externalRef is unique too, but a repeat isn't replayed: POST /customers with a used externalRef gets 409 conflict. Look the customer up with GET /customers?externalRef= first, or send an Idempotency-Key.