# Code examples

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:

```bash
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

```python
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

```javascript
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

```go
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

```bash
#!/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 `404` path (a contract id that is not yours) — confirm you surface it as
  "not found" rather than crashing on a missing field.
- A `429` path — force it by looping requests, and confirm your client waits
  for `Retry-After` instead of hot-looping.
- A multi-page walk — seed more rows than your page size, or set `limit=1`,
  and confirm you follow `nextCursor` to the end.
