Skip to content
trending

Guides

Your First AI API Call in Python: Step-by-Step Guide

Ten lines of code to query an AI model in Python: install the openai library, set your key, send messages, read the response. Includes streaming, error handling, and retry logic.

By 11 min read
Your First AI API Call in Python: Step-by-Step Guide

👁 114 reads

Your first query to an AI model in Python is just ten lines of code: you install the openai library, point it to an endpoint and key, send a list of messages, and read the response. Below we walk through everything from installing Python on Windows, through virtual environments, to streaming, error handling, and retry logic. We ran every script on October 4, 2026 on Windows 11 with Python 3.13.15 through OpenRouter and show you the real output. This is the third step in the AI API from scratch learning path.

What we tested on (October 4, 2026)

  • Windows 11 Pro, Python 3.13.15, libraries: openai 3.24.0, python-dotenv 1.2.4, pydantic 2.13.5.
  • Node.js 26.10 with openai library 7.27.0 (JavaScript version at the end of the article).
  • Model access: OpenRouter, default model openai/gpt-6-luna (0.10 / 0.50 USD per million tokens).
  • Cost of all runs in this article: approximately 0.005 USD.

Step 1. Install Python on Windows

  1. Go to python.org/downloads and download the current installer for Windows (Python 3.13 or newer; the openai library requires at least 3.10).
  2. In the first installer window, check Add python.exe to PATH. Without this, the terminal won’t find Python and this is the most common beginner problem.
  3. Alternative for terminal fans: winget install Python.Python.3.13.
  4. Open a new terminal (PowerShell or Windows Terminal) and check: python --version. You should see a version number, not the Microsoft Store window.

If the Microsoft Store opens instead of showing the version, go to Settings → Apps → Advanced app settings → App execution aliases and disable the python.exe and python3.exe aliases.

Step 2. Virtual Environment (venv)

venv is a separate “room” for one project’s libraries. Thanks to it, installing something for project A doesn’t break project B. You create it once per project:

mkdir my-api
cd my-api
python -m venv .venv
.venvScriptsActivate.ps1
pip install openai python-dotenv

After activation you’ll see (.venv) at the beginning of your terminal line. If PowerShell refuses to run the activation script, execute Set-ExecutionPolicy -Scope CurrentUser RemoteSigned once. In Git Bash you activate with source .venv/Scripts/activate, on Linux and Mac with source .venv/bin/activate.

Save your key in a .env file in the project directory (one line: OPENROUTER_API_KEY=sk-or-v1-...). We covered how to get it and secure it in the article API Key: how to get it and how not to burn it.

Step 3. First Query

We use the official OpenAI library, but with the OpenRouter address. The request format is the same, so with one key you have access to models from OpenAI, Anthropic, Google, DeepSeek and hundreds of others. Changing models is just changing one string.

"""First query to an AI model through OpenRouter (openai SDK)."""
import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()  # loads variables from .env file in current directory

client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key=os.environ["OPENROUTER_API_KEY"],
)

response = client.chat.completions.create(
    model=os.getenv("MODEL", "openai/gpt-6-luna"),
    messages=[
        {"role": "system", "content": "You answer briefly and concretely."},
        {"role": "user", "content": "Explain in two sentences what an API is."},
    ],
)

print(response.choices[0].message.content)
print("---")
print("model:", response.model)
print("input tokens:", response.usage.prompt_tokens)
print("output tokens:", response.usage.completion_tokens)
details = response.usage.completion_tokens_details
if details and details.reasoning_tokens:
    print("  including reasoning tokens:", details.reasoning_tokens)
cost = getattr(response.usage, "cost", None)  # OpenRouter adds cost in USD
if cost is not None:
    print(f"cost: {cost:.6f} USD")

What’s happening here, line by line:

  • load_dotenv() loads the key from the .env file into environment variables;
  • OpenAI(base_url=..., api_key=...) creates a client - without base_url it would connect to OpenAI servers;
  • messages is a list of messages: system sets the rules, user is the question (more about roles and parameters in the article System prompt, temperature, context window);
  • response.choices[0].message.content is the generated text;
  • usage is the token counter, and OpenRouter adds a cost field with the cost in dollars.

Output from our computer for the default GPT-6 Luna (we tested it in Polish):

API to zestaw reguł i narzędzi, dzięki którym różne programy mogą się ze sobą komunikować. Określa, jak wysyłać żądania i odbierać dane lub funkcje od innego systemu.
---
model: openai/gpt-6-luna
input tokens: 40
output tokens: 58
cost: 0.000033 USD

And the same question sent to Gemini 3.8 Flash (we only changed the MODEL variable):

API (Interfejs Programowania Aplikacji) to zestaw reguł, który umożliwia różnym programom i systemom bezpieczną komunikację oraz wymianę danych między sobą. Działa jak pośrednik, pozwalając jednej aplikacji korzystać z funkcji lub zasobów innej bez konieczności znajomości jej wewnętrznej budowy.
---
model: google/gemini-3.8-flash
input tokens: 29
output tokens: 462
  including reasoning tokens: 390
cost: 0.001754 USD

Look at the numbers: Gemini generated 462 output tokens for two sentences, because 390 of them are “thinking” before the answer. You pay for them like regular text, which is why this query cost 0.00175 USD, over 50 times more than on Luna. When counting costs, look at usage, not the length of the response. More in the article AI Tokens: what they are and how much they cost.

Step 4. Streaming: Live Response

Without streaming you wait until the model finishes and get everything at once. With streaming the text arrives piece by piece, just like in ChatGPT. It’s useful in chats and anywhere a human is watching the screen.

"""Streaming: response appears piece by piece, like in ChatGPT."""
import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=os.environ["OPENROUTER_API_KEY"])

stream = client.chat.completions.create(
    model=os.getenv("MODEL", "openai/gpt-6-luna"),
    messages=[{"role": "user", "content": "Give me 5 ideas for a cooking blog name. Just the list."}],
    stream=True,
    stream_options={"include_usage": True},  # last chunk will bring token counter
)

for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
    if chunk.usage:  # arrives at the very end
        print(f"n--- tokens: {chunk.usage.prompt_tokens} input, {chunk.usage.completion_tokens} output")
1. Prosto z Patelni
2. Szczypta Smaku
3. Gotuj z Głową
4. Widelcem po Talerzu
5. Kulinarna Przystań
--- tokens: 20 input, 439 output

With streaming the token counter doesn’t come automatically - you need to request it with the stream_options={"include_usage": True} parameter, and you’ll get it in the last chunk.

Step 5. Error Handling and Retry

In real programs queries sometimes fail: bad key, typo in model name, exceeded rate limit (429), temporary provider outage (5xx). The rule is simple:

  • errors on your side (400, 401, 403, 404) don’t retry - repeating will give the same result, fix the code;
  • transient errors (429, 500, 502, 503, broken connection) retry with increasing delay: 2 s, 4 s, 8 s, with a random addition so multiple programs don’t hit the server at the same second.
"""API error handling: 401, 429, 5xx and retry with exponential backoff."""
import os, sys, time, random
from dotenv import load_dotenv
import openai
from openai import OpenAI

load_dotenv()
client = OpenAI(
    # BASE_URL can be replaced with test server from tests/server_429.py
    base_url=os.getenv("BASE_URL", "https://openrouter.ai/api/v1"),
    api_key=os.environ.get("OPENROUTER_API_KEY", ""),
    max_retries=0,   # disable built-in retry to show our own
    timeout=60,      # seconds; without this a hung connection waits very long
)


def query(question, model, attempts=4):
    for attempt in range(1, attempts + 1):
        try:
            r = client.chat.completions.create(model=model, messages=[{"role": "user", "content": question}])
            return r.choices[0].message.content
        except openai.AuthenticationError as e:      # 401 - bad or disabled key
            sys.exit(f"401: check API key ({e.message})")
        except openai.PermissionDeniedError as e:    # 403 - no access (e.g. region, moderation)
            sys.exit(f"403: access denied ({e.message})")
        except openai.NotFoundError as e:            # 404 - bad model name or address
            sys.exit(f"404: no such model or address ({e.message})")
        except openai.BadRequestError as e:          # 400 - error in request body
            sys.exit(f"400: fix request ({e.message})")
        except (openai.RateLimitError, openai.InternalServerError,
                openai.APIConnectionError, openai.APITimeoutError) as e:
            # 429 (too many requests / no funds), 5xx (server-side failure), broken connection
            if attempt == attempts:
                raise
            delay = 2 ** attempt + random.random()   # 2, 4, 8 s + random fraction
            code = getattr(e, "status_code", "network")
            print(f"[attempt {attempt}] error {code}, waiting {delay:.1f} s...")
            time.sleep(delay)
        except openai.APIStatusError as e:           # other codes, e.g. 402 (no funds in OpenRouter)
            sys.exit(f"{e.status_code}: {e.message}")


if __name__ == "__main__":
    model = sys.argv[1] if len(sys.argv) > 1 else os.getenv("MODEL", "openai/gpt-6-luna")
    print(query("What is 17 * 23? Just the result.", model))

We tested four scenarios. Normal query:

391

Fake key and non-existent model (note that OpenRouter responds to a bad model with 400, not 404):

401: check API key (Error code: 401 - {'error': {'message': 'User not found.', 'code': 401}})
400: fix request (Error code: 400 - {'error': {'message': 'openai/gpt-7-does-not-exist is not a valid model ID', 'code': 400}})

The hardest to test is 429, because you’d have to actually flood the server with requests. So we wrote a mock server that rejects the first two requests with code 429 and handles the third. The script doesn’t know it’s a fake:

"""Mock API server: first two requests get 429, third gets proper response.
Used to verify that retry in 03_errors_and_retry.py works - without spending money.

Window 1:  python tests/server_429.py
Window 2:  set BASE_URL=http://127.0.0.1:8429/v1   (PowerShell: $env:BASE_URL="http://127.0.0.1:8429/v1")
           python 03_errors_and_retry.py
"""
import json
from http.server import BaseHTTPRequestHandler, HTTPServer

counter = {"n": 0}


class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        self.rfile.read(int(self.headers.get("Content-Length", 0)))
        counter["n"] += 1
        if counter["n"] <= 2:
            code, content = 429, {"error": {"message": "Rate limit exceeded (test)", "code": 429}}
        else:
            code, content = 200, {
                "id": "test-1", "object": "chat.completion", "created": 0, "model": "test",
                "choices": [{"index": 0, "finish_reason": "stop",
                             "message": {"role": "assistant", "content": "391"}}],
                "usage": {"prompt_tokens": 10, "completion_tokens": 1, "total_tokens": 11},
            }
        body = json.dumps(content).encode()
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)
        print(f"request {counter['n']} -> {code}")


HTTPServer(("127.0.0.1", 8429), Handler).serve_forever()
[attempt 1] error 429, waiting 2.8 s...
[attempt 2] error 429, waiting 4.2 s...
391

Retry worked: two rejections, delays of 2.8 s and 4.2 s, and on the third try we got the result. The openai library also has built-in retry (default 2 times, max_retries parameter), which we disabled in the script to show the mechanism. In our own programs we usually leave the built-in one and set max_retries=5.

Three additional tips from practice:

  • Always set timeout. By default the library waits up to 10 minutes. With long responses from reasoning models 60 s is sometimes not enough, so adjust it to your task.
  • Log the cost of every query. The usage.cost field saved to a file after a week will tell you more than any calculator.
  • Distinguish 429 from lack of funds. In OpenAI, exceeded spending limit also returns 429 (code project_spend_limit_exceeded), and retry won’t fix it.

The Same in JavaScript (Node.js)

The openai library for Node.js has almost identical names. We installed Node.js 26.10, in the project directory executed npm install openai dotenv (versions 7.27.0 and 18.0.5), in package.json set "type": "module". The script does three things: normal query, streaming, and catching an error.

// First query in Node.js through OpenRouter: normal response and streaming.
// Run in js directory:  npm install   then   node first_query.mjs
import "dotenv/config";          // loads .env from current directory
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://openrouter.ai/api/v1",
  apiKey: process.env.OPENROUTER_API_KEY,
});
const model = process.env.MODEL ?? "openai/gpt-6-luna";

// 1. Normal query
const r = await client.chat.completions.create({
  model,
  messages: [
    { role: "system", content: "You answer briefly." },
    { role: "user", content: "What's the difference between JavaScript and Java? One sentence." },
  ],
});
console.log(r.choices[0].message.content);
console.log(`--- ${r.usage.prompt_tokens} + ${r.usage.completion_tokens} tokens, cost ${r.usage.cost} USDn`);

// 2. Streaming
const stream = await client.chat.completions.create({
  model,
  messages: [{ role: "user", content: "Name 3 major rivers. Just names separated by commas." }],
  stream: true,
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
console.log();

// 3. Error handling: bad model
try {
  await client.chat.completions.create({ model: "does/not-exist", messages: [{ role: "user", content: "test" }] });
} catch (e) {
  if (e instanceof OpenAI.APIError) console.log(`Error ${e.status}: ${e.message}`);
  else throw e;
}
JavaScript to dynamiczny język używany głównie w przeglądarkach i aplikacjach webowych, a Java to osobny, statycznie typowany język kompilowany do uruchamiania na JVM.
--- 39 + 54 tokens, cost 0.0000309 USD

Wisła, Odra, Warta
Error 400: 400 does/not-exist is not a valid model ID

The differences from Python are cosmetic: baseURL instead of base_url, await before the query, for await with streaming. In Node.js you also keep the key in .env and never in page code that goes to the browser.

Ready Scripts to Download

All scripts from this article (and from other parts of the learning path) are in the ai-api-starter package with README and .env.example file. Just unpack, enter your key, and run python 01_first_query.py.

What’s Next

Frequently Asked Questions

Do I have to use OpenRouter for the OpenAI API?

No. If you have an OpenAI key, remove base_url and provide the model without prefix (gpt-6-luna). An example of direct connection to OpenAI, Anthropic and Google is in the article about API keys.

I get ModuleNotFoundError: No module named ‘openai’. What am I doing wrong?

You installed the library in a different environment than where you’re running the script. Activate venv (.venvScriptsActivate.ps1) and install again, or run the script through .venvScriptspython.

KeyError: ‘OPENROUTER_API_KEY’ - why?

The script didn’t find the key. Check if the file is named exactly .env (Windows likes to add .txt), if it’s in the directory from which you’re running the script, and if there are no spaces around the equals sign inside.

Chat Completions or Responses API?

OpenAI recommends Responses API for new projects, but Chat Completions still works and is the standard understood by OpenRouter, DeepSeek, Mistral and local servers like Ollama. For learning we chose Chat Completions because the same code works everywhere.

How much does learning the API cost?

Pennies. All runs from this article cost us about half a cent. Free models in OpenRouter (with :free suffix) and Gemini’s free tier let you practice for zero dollars.

Google · Your sourcesQuantslant higher in your Google - one clickAdd to preferred sources →

Sources and Verification Date

Kinetyra

// read next

More on this from QUANTSLANT

Piotr Olszewski

Editor and publisher · Quantslant

I edit and publish Quantslant. AI tools help with research, drafts and translation; choosing topics, running the tests, checking facts and the decision to publish are mine, and so is the responsibility for what appears here.

read nextOpenRouter Guide: One API Key for 460+ AI ModelsguidesOpenRouter Guide: One API Key for 460+ AI ModelsOpenRouter gives you one API key and balance for 460+ AI models from OpenAI, Anthropic, Google, DeepSeek and more. 5.5% fee, 17 free models, same token prices.

Most read

  1. 1Best AI Video Generator Apps for Your Phone (2026)
  2. 2Claude Opus 5.5 Motion Graphics: 60s Showreel from One Prompt
  3. 3Wan 2.2 on RTX 4090: timings, VRAM and four test clips
// site map

This page in other languages6

Quantslant in numbers 1,423posts in archive22news from the last 7 days18video models in the observatory
🎬 AI Video Benchmarks: Higgsfield DoP vs OpenAI Sora • Higgsfield vs Runway Gen-3 • AI Video Credit Calculator
× ‹ enlarge ›