Tools, OpenAPI and code samples
GET /v1/openapi.json
No permission, no key. It is the only open route of the API.
| Document | Detail |
|---|---|
| Contents | OpenAPI 3.1: every route, parameter, body, response and error, plus x-scopes (the permission catalogue) and x-rate-limits |
| Generation | reflected from the controllers when the API starts. It always describes the code serving it |
| Cache | Cache-Control: public, max-age=300, with an ETag. An If-None-Match answers 304 |
The document's servers entry is /, a relative URL. After an import, set the base to https://api.freshperf.fr: without it your client will build paths relative to the current host.
| Use | How |
|---|---|
| Browse it | freshperf.fr/api-reference. Search, samples, try-it console. Calls go from your browser to api.freshperf.fr |
| Import it | Postman, Insomnia, Bruno and Hoppscotch take the URL directly. Set the bearer token at collection level |
| Generate a client | openapi-generator, openapi-typescript, oapi-codegen, Kiota |
bash
export FRESHPERF_KEY="fpk_9al41uPRzeglnYvHa3YfvsK86OfQwx6BmC2EKmI0Vhw" api() { curl -sS -H "Authorization: Bearer $FRESHPERF_KEY" "$@"; } # Every service with its status api https://api.freshperf.fr/v1/services | jq -r '.data[] | "\(.code)\t\(.status)\t\(.name)"' # Restart one api -X POST -H "Content-Type: application/json" -d '{"action":"restart"}' \ https://api.freshperf.fr/v1/services/SRV-TJUAQ1-1951/power # Download the last invoice, if there is one code=$(api "https://api.freshperf.fr/v1/invoices?limit=1" | jq -r '.data[0].code // empty') [ -n "$code" ] && api -o "$code.pdf" "https://api.freshperf.fr/v1/invoices/$code/pdf"
Python
import os, uuid, requests API = "https://api.freshperf.fr/v1" session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['FRESHPERF_KEY']}" def get(path, **params): r = session.get(f"{API}{path}", params=params) if not r.ok: err = r.json().get("error", {}) raise RuntimeError(f"{r.status_code} {err.get('code')}: {err.get('message')} " f"(request {r.headers.get('X-Request-Id')})") return r.json() def each(path, limit=100, **params): """Walks a cursor list. Not suitable for /account/notifications.""" cursor = None while True: page = get(path, cursor=cursor, limit=limit, **params) yield from page["data"] cursor = page["pagination"]["nextCursor"] if not cursor: break for service in each("/services", status="ACTIVE"): print(service["code"], service["nextBillingAt"]) # Notifications page by page, with a ceiling of 50 page1 = get("/account/notifications", page=1, limit=50) print(page1["data"]["total"], len(page1["data"]["items"])) # Pay an invoice from the balance. The idempotency key is made once and kept # with the task: it is what makes the retry safe. key = str(uuid.uuid4()) r = session.post(f"{API}/invoices/INV-202608-0082/pay", json={"method": "balance"}, headers={"Idempotency-Key": key}) print(r.status_code, r.json())
Node.js
const API = "https://api.freshperf.fr/v1"; async function call(method, path, body, extra = {}) { const res = await fetch(API + path, { method, headers: { Authorization: `Bearer ${process.env.FRESHPERF_KEY}`, ...(body ? { "Content-Type": "application/json" } : {}), ...extra, }, body: body ? JSON.stringify(body) : undefined, }); const json = await res.json().catch(() => ({})); if (!res.ok) { const e = json.error ?? {}; throw new Error(`${res.status} ${e.code}: ${e.message} (request ${res.headers.get("x-request-id")})`); } return json; } const { data: me } = await call("GET", "/me"); console.log(me.key.scopes); const { data: status } = await call("GET", "/services/SRV-TJUAQ1-1951/status"); if (status.status !== "running") { await call("POST", "/services/SRV-TJUAQ1-1951/power", { action: "start" }); } // An order paid from the balance. The values come from the catalogue. const idempotencyKey = crypto.randomUUID(); const order = await call("POST", "/orders", { lines: [{ product: "vps-1", recurrence: "monthly", characteristics: { os: "debian-13", location: "paris", authType: "PASSWORD", authValue: process.env.VPS_ROOT_PASSWORD, }, }], payment: { method: "balance" }, }, { "Idempotency-Key": idempotencyKey }); console.log(order.data.order.orderNumber, order.data.order.serviceCodes);
Habits that save time
- The key in an environment variable or a secret manager, never in the code.
- Log the
X-Request-Idof every non-2xx response. It is what support will ask for. - Page with
pagination.nextCursor, except on/account/notifications, which pages bypage. - Retry
429afterRetry-After, and5xxwith exponential backoff. - On the four routes that move money, keep the same idempotency key for every retry of one task. See the table of refusals that release the key in Rate limits and idempotency.
- On
409 IDEMPOTENCY_IN_PROGRESS, waitRetry-Afterand replay the same request: the first call will finish and you will get its result. - Poll
/statusevery 5 to 10 seconds at most: each call asks the infrastructure live. - Re-read the key's log on the first day an integration runs. A refusal sitting there is a forgotten permission or restriction.