Code examples
A generated client, paging, collecting on an invoice and testing, in Python, Node, Go and shell.
Working snippets for the two pieces every integration needs — walking pages and retrying correctly — plus a typed-client shortcut.
Generate a client from the spec
The fastest path is not to hand-write a client at all. The spec is standard OpenAPI 3.1:
curl -s "$FYNEX_API_BASE/openapi.json" -o fynex-billing.json
# TypeScript types
npx openapi-typescript fynex-billing.json -o fynex-billing.d.ts
# Python / Go / Java / … via openapi-generator
openapi-generator generate -i fynex-billing.json -g python -o ./fynex-billing-client
Generated models keep minor-unit amounts as integers and decimal quantities as
strings, which is what you want. If your generator maps quantity or
taxRate to a float, override it to a decimal type.
Python: page through invoices
import os
import time
import requests
BASE = os.environ["FYNEX_API_BASE"] # e.g. https://api.fynex.ai/billing-api/v1
SESSION = requests.Session()
SESSION.headers["Authorization"] = f'Bearer {os.environ["FYNEX_API_KEY"]}'
def request(method, path, **kwargs):
"""One request with the retry policy: honour Retry-After on 429,
back off on 5xx, never retry other 4xx."""
delay = 1.0
for attempt in range(6):
response = SESSION.request(method, f"{BASE}{path}", timeout=30, **kwargs)
if response.status_code == 429:
# Retry-After is authoritative; retrying sooner just deepens the overage.
time.sleep(int(response.headers.get("Retry-After", "1")))
continue
if response.status_code >= 500:
time.sleep(delay)
delay *= 2
continue
if not response.ok:
# 400/401/403/404/422 — the request must change, so retrying is pointless.
raise RuntimeError(f"{response.status_code}: {response.json().get('error')}")
return response
raise RuntimeError("giving up after repeated throttling or server errors")
def iter_invoices(**filters):
"""Yield every invoice, newest first, following the keyset cursor."""
before_id = None
while True:
params = {"limit": 100, **filters}
if before_id:
params["cursor"] = before_id
page = request("GET", "/invoices", params=params).json()
for invoice in page["invoices"]:
yield invoice
if not page["hasMore"]:
return
before_id = page["nextCursor"]
outstanding = [
inv for inv in iter_invoices(origin="usage")
if inv["status"] in ("issued", "sent") and inv["collectibleMinor"] > 0
]
total_minor = sum(inv["collectibleMinor"] for inv in outstanding)
print(f"{len(outstanding)} open invoices, {total_minor / 100:.2f} outstanding")
The last line divides by 100 only because the example is EUR. Currencies have
different minor-unit scales — divide by the scale of the invoice's own
currency, or better, keep the integer and format at the edge.
Node: collect on an invoice
const BASE = process.env.FYNEX_API_BASE; // https://api.fynex.ai/billing-api/v1
async function call(method, path, options = {}) {
for (let attempt = 0; attempt < 6; attempt++) {
const response = await fetch(`${BASE}${path}`, {
method,
...options,
headers: {
Authorization: `Bearer ${process.env.FYNEX_API_KEY}`,
...options.headers,
},
});
if (response.status === 429) {
const wait = Number(response.headers.get("Retry-After") ?? 1);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
if (response.status >= 500) {
await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
continue;
}
const body = await response.json();
if (!response.ok) throw new Error(`${response.status}: ${body.error}`);
return body;
}
throw new Error("giving up after repeated throttling or server errors");
}
// Safe to retry as-is: the invoice's durable link association makes repeated
// and simultaneous calls converge on the same payment link.
const { invoice, paymentLinkUrl } = await call("POST", `/invoices/${invoiceId}/send`);
console.log(`Invoice ${invoice.invoiceNumber}: ${paymentLinkUrl}`);
Go: usage against plan limits
type MetricUsage struct {
MetricName string `json:"metricName"`
Used string `json:"used"` // decimal string
IncludedUnits string `json:"includedUnits"` // decimal string
CapQuantity string `json:"capQuantity,omitempty"`
CapMode string `json:"capMode,omitempty"`
PercentOfPlan string `json:"percentOfPlan,omitempty"`
PeriodStart string `json:"periodStart,omitempty"`
PeriodEnd string `json:"periodEnd,omitempty"`
}
type ContractUsage struct {
ContractID int64 `json:"contractId"`
Metrics []MetricUsage `json:"metrics"`
}
func overageMetrics(ctx context.Context, client *http.Client, baseURL, key string, contractID int64) ([]MetricUsage, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
fmt.Sprintf("%s/contracts/%d/usage", baseURL, contractID), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var apiErr struct {
Error string `json:"error"`
}
_ = json.NewDecoder(resp.Body).Decode(&apiErr)
return nil, fmt.Errorf("usage snapshot: %d: %s", resp.StatusCode, apiErr.Error)
}
var usage ContractUsage
if err := json.NewDecoder(resp.Body).Decode(&usage); err != nil {
return nil, err
}
var over []MetricUsage
for _, m := range usage.Metrics {
// Decimal, not float: these quantities carry fractional precision.
pct, err := decimal.NewFromString(m.PercentOfPlan)
if err != nil || m.PercentOfPlan == "" {
continue // no plan allowance configured for this metric
}
if pct.GreaterThan(decimal.NewFromInt(100)) {
over = append(over, m)
}
}
return over, nil
}
Shell: download every PDF for a month
#!/usr/bin/env bash
set -euo pipefail
BASE="${FYNEX_API_BASE:?set FYNEX_API_BASE}"
AUTH="Authorization: Bearer ${FYNEX_API_KEY:?set FYNEX_API_KEY}"
cursor=""
while :; do
url="$BASE/invoices?limit=100"
[ -n "$cursor" ] && url="$url&cursor=$cursor"
page="$(curl -sf -H "$AUTH" "$url")"
# Every issued document renders, including void ones and credit notes —
# they are part of the audit trail. Filter here if you only want live ones.
echo "$page" | jq -r '.invoices[] | "\(.id) \(.invoiceNumber)"' |
while read -r id number; do
curl -sf -H "$AUTH" "$BASE/invoices/$id/pdf" -o "invoices/$number.pdf"
done
[ "$(echo "$page" | jq -r '.hasMore')" = "true" ] || break
cursor="$(echo "$page" | jq -r '.nextCursor')"
sleep 1 # stay clear of the per-seller rate limit while bulk-downloading
done
Testing your integration
Point everything at a sk_test_… key while your account is still in demo
mode. Going live is two steps, not one: Fynex switches the account to live
mode, and you swap in an sk_live_… key. From that moment the test key no
longer authenticates (401), so make the key a configuration value rather
than a constant. The billing records themselves belong to the account — a
live key does not reveal a second, hidden dataset.
Worth exercising before you ship:
- A
404path (a contract id that is not yours) — confirm you surface it as "not found" rather than crashing on a missing field. - A
429path — force it by looping requests, and confirm your client waits forRetry-Afterinstead of hot-looping. - A multi-page walk — seed more rows than your page size, or set
limit=1, and confirm you follownextCursorto the end.