How to connect an AI model to your app: key, SDK, streaming, errors
A step-by-step guide for developers: where to keep the key, how to call a model from Python and Node.js, how to stream answers correctly, which errors to handle, and how to track the cost of each request.
6 min · · development · API keys
"How to connect an AI model to an app" is where almost every integration starts: a chat in a user dashboard, product description generation, request triage. The model call itself is a dozen lines of code; the real work is in the details: where to keep the key, how to stream the answer, what to do with errors, and how to stay in control of spending. Below is a step-by-step setup using Zukko as the example, with code in Python and Node.js.
How to connect an AI model to your app: the overall setup
browser / mobile app → your backend → https://api.zukko.pro → model
The main rule: the key lives only on the backend. If you put it in page JavaScript or a mobile app build, anyone can extract it and spend your balance. The client calls your server, the server checks the user, applies your own restrictions, and only then calls the model.
Step 1. Get a key
- Create a key in the dashboard on the API keys page. The full key (
fetch_...) is shown only once — save it to a secrets manager right away. - Give the key a name, a daily and monthly dollar limit, and a list of allowed models (
allowed_models). - Use separate keys for development and production: a leaked or runaway test key won't affect the production one.
- Top up the balance: a request only starts if the account has at least $0.01. Top-ups start at $1; payment from Russia is available via SBP, Russian cards and cryptocurrency.
Step 2. The key in environment variables
export ZUKKO_API_KEY="fetch_your_key"In Docker and CI, pass it as a secret, not as part of the image or repository. If you use the official OpenAI Python SDK, you can set OPENAI_BASE_URL=https://api.zukko.pro/v1 and OPENAI_API_KEY, and the client will pick them up itself.
Step 3. The first request via SDK
Zukko works with the official SDKs: only the address and key change. For the OpenAI SDK the address is https://api.zukko.pro/v1, for the Anthropic SDK https://api.zukko.pro (no /v1, the SDK appends it). Any model can be called in either format.
Python (pip install openai):
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.zukko.pro/v1", api_key=os.environ["ZUKKO_API_KEY"])
resp = client.chat.completions.create(
model="gpt-6.1-sol",
messages=[
{"role": "system", "content": "You are an online store assistant. Keep answers short."},
{"role": "user", "content": "How do I return an item?"},
],
max_tokens=500,
)
print(resp.choices[0].message.content)Node.js (npm install openai, ES modules):
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.zukko.pro/v1",
apiKey: process.env.ZUKKO_API_KEY,
});
const resp = await client.chat.completions.create({
model: "gpt-6.1-sol",
messages: [{ role: "user", content: "How do I return an item?" }],
max_tokens: 500,
});
console.log(resp.choices[0].message.content);The model list is at GET https://api.zukko.pro/v1/models (no key needed). Keep the model name in config so you can change it without deploying code.
Step 4. Streaming the answer
For chats, turn on stream: true — the user sees text as it's generated. In the OpenAI format, Zukko adds stream_options.include_usage itself, so the last chunk of the stream arrives with usage and an empty choices. Code that reads chunk.choices[0] without a check will crash on it.
Python:
stream = client.chat.completions.create(
model="gpt-6.1-sol",
messages=[{"role": "user", "content": "Tell me about delivery terms"}],
max_tokens=500,
stream=True,
)
for chunk in stream:
if chunk.choices:
print(chunk.choices[0].delta.content or "", end="", flush=True)
elif chunk.usage:
print(f"\n[input {chunk.usage.prompt_tokens}, output {chunk.usage.completion_tokens}]")Node.js:
const stream = await client.chat.completions.create({
model: "gpt-6.1-sol",
messages: [{ role: "user", content: "Tell me about delivery terms" }],
max_tokens: 500,
stream: true,
});
for await (const chunk of stream) {
if (chunk.choices.length > 0) {
process.stdout.write(chunk.choices[0].delta?.content ?? "");
} else if (chunk.usage) {
console.log("\ntokens:", chunk.usage.prompt_tokens, chunk.usage.completion_tokens);
}
}Two facts about streaming from the Zukko docs:
- The 200 status is sent before the model starts answering. If an error happens later, it arrives inside the stream as an event with an
errorfield — check the events, not just the HTTP status. - If the client drops the connection, Zukko still reads the model's answer to the end and charges for everything generated. Limit spending with
max_tokens, not by cutting the stream.
Step 5. Error handling
Zukko returns errors in the OpenAI format: an HTTP status and JSON with error.code. Handle errors by status and code, not by message text.
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 401 | — | wrong or revoked key | check the key, don't retry |
| 402 | insufficient_balance | insufficient balance | top up, set yourself an alert |
| 403 | model_not_allowed_for_key | model not on the key's list | fix the config or the key |
| 404 | model_not_found | unknown model name | check against GET /v1/models |
| 413 | payload_too_large | request too large | reduce the input |
| 429 | key_daily_limit_reached / key_monthly_limit_reached | key limit used up | wait for reset or raise the limit |
| 429 | provider's code | limit at the model provider | retry with a pause |
| 503 | model_disabled | model switched off | pick another from GET /v1/models |
| 503 | other code | provider unavailable | retry with a pause or switch models |
| 529 | provider's code | model provider overloaded, status passed through by the gateway | same as 503: retry with a pause or switch models |
Zukko has no general rate limit of its own: a 429 from Zukko means only the key limits you set yourself. Why provider overload can arrive as either a 529 or a 503 is covered in the article the Claude 529 overloaded error.
The official OpenAI SDKs retry a request 2 times by default on connection errors, 429 and 5xx (configurable via max_retries in Python and maxRetries in Node.js). Retries won't help with an exhausted key limit or model_disabled — handle those in your own code:
import openai
FALLBACK_MODEL = "deepseek-v4.1-flash"
def ask(messages, model="gpt-6.1-sol"):
try:
resp = client.chat.completions.create(model=model, messages=messages, max_tokens=500)
return resp.choices[0].message.content
except openai.APIStatusError as e:
if e.status_code == 503 and e.code == "model_disabled" and model != FALLBACK_MODEL:
return ask(messages, FALLBACK_MODEL)
if e.status_code == 402:
raise RuntimeError("Insufficient Zukko balance") from e
if e.status_code == 429 and e.code in ("key_daily_limit_reached", "key_monthly_limit_reached"):
raise RuntimeError("API key limit reached") from e
raiseIn Node.js the same fields are available as err.status and err.code on OpenAI.APIError. A detailed retry and fallback-model setup is in the article LLM API error handling.
Step 6. Limits
- Key limits in Zukko are soft: before a request, the amount already spent is compared with the cap. The last request let through may slightly exceed the limit, and parallel requests started before it was reached will go through. The daily spend resets at 00:00 UTC, the monthly one at the start of the month UTC. More in the key limits and cache docs.
- Per-user limits belong in your app: requests per minute, maximum message length, a daily budget per account. There's one key for the whole app, and without this one active user can eat the limit for everyone.
max_tokensin every request is a simple way to cap the cost of a single answer.
Step 7. Tracking spending
For regular (non-streaming) responses, Zukko returns the X-Zukko-Cost-Micros header — the request cost in micro-dollars. It's convenient to store it in your database next to the user id:
raw = client.chat.completions.with_raw_response.create(
model="gpt-6.1-sol",
messages=[{"role": "user", "content": "Hi!"}],
max_tokens=200,
)
resp = raw.parse()
cost_micros = int(raw.headers.get("x-zukko-cost-micros", "0"))
print(resp.choices[0].message.content, cost_micros / 1_000_000, "$")In Node.js the header is available via .withResponse(): response.headers.get("x-zukko-cost-micros"). Streaming responses don't have this header — calculate from usage in the last chunk and the model's prices, or check the history in the dashboard and via GET /v1/usage.
What it costs. Prices as of October 7, 2026, USD per 1M tokens. An app makes 10,000 requests a month, averaging 1,500 input and 400 output tokens. Formula: 10,000 × (1,500 × input + 400 × output) / 1,000,000.
gpt-6.1-sol($0.27 / $1.32): 10,000 × (405 + 528) / 1,000,000 = $9.33 a month. One such request without caching is 1,500 × 0.27 + 400 × 1.32 = 933 micro-dollars; that's the number to compare theX-Zukko-Cost-Microsheader against.deepseek-v4.1-flash($0.013 / $0.038): 10,000 × (19.5 + 15.2) / 1,000,000 ≈ $0.35 a month.
If the prompt carries a long repeated instruction, account for caching: for GPT, cache reads cost less than input and show up in prompt_tokens_details.cached_tokens, while for Claude the cache discount works only in the Anthropic format. For the current list and prices, call GET https://api.zukko.pro/v1/models or open the Models page in the dashboard. More code examples are in the Python docs.
FAQ
Can I call the API directly from a browser or mobile app?
Better not: the key would end up with your users. Make calls from the backend and hand the client the finished answer or stream.
Which SDK should I use — OpenAI or Anthropic?
For most apps the OpenAI SDK is simpler: all models are available through it. The Anthropic SDK makes sense if you work with Claude and want the cache discount on a long system prompt.
Why does streaming crash with IndexError at the end?
The last chunk of the stream arrives with usage and an empty choices. Check if chunk.choices: (in Node.js, chunk.choices.length > 0) before accessing chunk.choices[0].
How do I find the cost of a streaming response?
Streams don't carry the X-Zukko-Cost-Micros header. Take usage from the last chunk and multiply by the model's prices, or look up the request in the spending history in the dashboard or via GET /v1/usage.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.
- DeepSeek API in Python: Requests, Streaming, JSON and Error HandlingReady code for DeepSeek V4.1 Flash and V4 Pro on the OpenAI SDK: a request, streaming, JSON parsing and careful error handling.
- Gemini API in Python: requests via the OpenAI SDK and OpenAI formatA working Gemini API example in Python with no extra library: change base_url in the OpenAI SDK, add streaming and JSON, and compare with google-genai.
- Anthropic SDK for Python: base_url, key and a first request via a gatewayThe official anthropic package for Python just needs a different address. We cover base_url, the key, streaming, token counting, caching and errors.