Generate a Client from the OpenAPI Spec
There is no official SDK yet. Generate a typed client from the published spec, and handle the two things a generator will not do for you.
Generate a Client from the OpenAPI Spec
QuotaStack does not have an official SDK yet. Until it does, the OpenAPI spec is the contract, and generating a client from it is the fastest way to get typed calls in your language.
The spec is published here, and it is the same file the API is built from:
https://quotastack.io/openapi.yaml
Read this before you pick a generator
The spec is OpenAPI 3.1.1.
This matters more than it sounds. Many generators still target OpenAPI 3.0. Given a 3.1 document, a lot of them do not fail — they run, they emit code, and they get the types wrong.
The reason is how the two versions write “this field can be null”. Version 3.0 used a nullable: true flag. Version 3.1 uses a list of types instead:
actual_units: { type: [integer, 'null'], format: int64 }
A 3.0-only generator does not understand that list. It commonly falls back to any, object, or unknown. You get a client that compiles and lies: every nullable field in the API loses its type.
So check your generator supports 3.1 before you trust its output. The two below were run against this spec and produce correct types.
TypeScript
Verified with openapi-typescript 7.13.0 on 2026-07-28.
npx openapi-typescript https://quotastack.io/openapi.yaml -o schema.d.ts
That writes types only. Pair it with openapi-fetch for a small typed client that uses those types.
Check the output got nullables right before going further:
grep -A 2 "actual_units" schema.d.ts
You should see the field typed as both optional and nullable:
actual_units?: number | null;
actual_cost?: number | null;
Both parts matter. ? means the API may leave the key out entirely, which it does for a reservation that has not been committed. | null means the key may be present and null. If your generator gave you any here, it does not support 3.1 — pick another one.
Python
Verified with openapi-python-client 0.29.0 on 2026-07-28.
python3 -m venv venv
./venv/bin/pip install openapi-python-client
./venv/bin/openapi-python-client generate --url https://quotastack.io/openapi.yaml
This one generates a full client, not just types: a module per API tag, and a class per model.
It handles the 3.1 nullable form correctly, and it goes one step further than most:
actual_units: int | None | Unset = UNSET
Three states, not two. UNSET means the API did not send the key. None means it sent null. That distinction is real in this API, so it is worth having.
If the generator prints ruff is not in PATH, it only skipped formatting the output. The client is still generated.
Authenticate
Every request needs your API key in a header:
X-API-Key: qs_live_...
The key prefix chooses the environment. qs_live_ is live, qs_test_ is sandbox. Before you point a generated client at sandbox, read Environments & the Shared Catalog — your customers are separated between environments, but your plans, metrics, and prices are not.
The two things a generator will not do for you
A generated client gives you types and request shapes. It does not know this API’s two rules.
1. Every POST and PATCH needs an Idempotency-Key
This is required, not advisory. A POST or PATCH without an Idempotency-Key header returns 422.
Generators do not add it, because it is not part of any request body. Wrap your client so every write sends one:
import createClient from "openapi-fetch";
import type { paths } from "./schema";
const client = createClient<paths>({
baseUrl: "https://api.quotastack.io",
headers: { "X-API-Key": process.env.QUOTASTACK_API_KEY! },
});
// One key per logical operation. Reuse it when retrying that same
// operation — that is what makes the retry safe.
function idempotent() {
return { "Idempotency-Key": crypto.randomUUID() };
}
await client.POST("/v1/customers/{customer_id}/credits/grant", {
params: { path: { customer_id: id }, header: idempotent() },
body: { credits: 100_000, source: "manual", reason: "Welcome bonus" },
});
Generate the key once per operation and reuse it across retries of that operation. A fresh key on a retry is a second operation, and it will grant twice. See Idempotency for the full rules.
2. List endpoints are cursor-paginated
A list call returns one page. Read pagination.next_cursor and pass it back as ?cursor=, until pagination.has_more is false:
let cursor: string | null = null;
const all = [];
do {
const { data } = await client.GET("/v1/customers", {
params: { query: { limit: 100, ...(cursor ? { cursor } : {}) } },
});
all.push(...(data?.data ?? []));
cursor = data?.pagination.next_cursor ?? null;
} while (cursor);
Without the cursor you will fetch the first page forever.
Keeping the client current
Regenerate when the API changes. The spec at /openapi.yaml is the same file the API is built from, so it moves when the API moves. Regenerating is one command, and your type checker will show you what changed.
An official SDK is on the roadmap. When it ships it will handle the idempotency keys and pagination above for you.
Loading…