অধ্যায় 3 · HTTP ও ওয়েব API
ওয়েব API ও REST: ডকুমেন্টেশন পড়া আর curl দিয়ে পরীক্ষা
- পৃষ্ঠা 13 / 22
- 23 মিনিট পড়া
API (Application Programming Interface) হলো একটা চুক্তি, যার মাধ্যমে একটা প্রোগ্রাম আরেকটাকে ব্যবহার করতে পারে। ওয়েব API হলো HTTP-র ওপর দিয়ে দেওয়া সেই চুক্তি: কিছু URL-এর তালিকা, সেগুলো কোন মেথড নেয়, কী পাঠাতে হবে আর কী ফেরত আসবে। আবহাওয়ার ডেটা, পেমেন্ট, ম্যাপ, GitHub, আর প্রতিটা হোস্টেড LLM (OpenAI, Anthropic, Google, Mistral, …) ব্যবহার করা হয় ওয়েব API দিয়ে। একটা AI প্রোডাক্ট বানাতে গেলে আপনি ওয়েব API ব্যবহারও করবেন, নিজে একটা দেবেনও।
এই পাতায় আগের দুই পাতার শেখা কাজে লাগবে। আপনি নিজের কম্পিউটারে BitByte Practice API সেট আপ করবেন, এর ডকুমেন্টেশন পড়বেন ঠিক যেভাবে OpenAI বা GitHub-এরটা পড়তেন, আর curl দিয়ে এটা ব্যবহার করবেন: পাবলিক এন্ডপয়েন্ট, API কি, বেয়ারার টোকেন, এরর রেসপন্স আর একটা রেট লিমিট। শেষে একটা নতুন API-র ডকুমেন্টেশন আপনার কাছে লেখার দেয়াল নয়, একটা মানচিত্রের মতো লাগার কথা।
যা শিখবেন
- একটা API-কে "ওয়েব" আর "REST" কী বানায়: রিসোর্স, এন্ডপয়েন্ট, বেস URL আর ভার্সন।
- Practice API কীভাবে চালাবেন, আর
/docs-এ এর OpenAPI ডকুমেন্টেশন কীভাবে পড়বেন। - পাবলিক বনাম অথেনটিকেটেড এন্ডপয়েন্ট: API কি আর বেয়ারার টোকেন, আর এগুলো ছাড়া কী এরর পাবেন।
- curl গভীরভাবে: যে ডজনখানেক অপশন আসলেই ব্যবহার করবেন, আর স্ক্রিপ্টের জন্য এক্সিট কোড।
- ব্যবহারের সীমা দেখতে কেমন (429 আর
Retry-After), আর কোন কোন GUI ক্লায়েন্ট আছে।
API, ওয়েব API, REST
প্রোগ্রামগুলোর কথা বলার যেকোনো নির্ধারিত উপায়ই একটা API: json মডিউলের ফাংশনগুলো ওই মডিউলের API। ওয়েব API-তে পৌঁছানো হয় নেটওয়ার্কের ওপর HTTP দিয়ে, তাই যেকোনো মেশিনের যেকোনো ভাষা এটা ব্যবহার করতে পারে। আজকের বেশিরভাগ ওয়েব API REST ধাঁচ (REpresentational State Transfer) মেনে চলে, যার বাস্তব মানে:
- API সাজানো থাকে রিসোর্স ঘিরে, অর্থাৎ নাম আছে এমন জিনিস: কোর্স, রিভিউ, ইউজার, মেসেজ। পাথগুলো বিশেষ্য, সাধারণত বহুবচন:
/v1/courses,/v1/courses/2। - রিসোর্স নিয়ে কী করতে হবে, তা বলে HTTP মেথড (আগের পাতা): পড়তে GET, তৈরি করতে POST, ইত্যাদি। REST API-তে
/getCourseবা/deleteReviewদেখবেন না। - রিসোর্স পাঠানো হয় রিপ্রেজেন্টেশন হিসেবে, প্রায় সবসময় JSON।
- প্রতিটা রিকোয়েস্ট স্টেটলেস: দরকারি সবকিছু, ক্রেডেনশিয়ালসহ, সে নিজেই বহন করে।
একটা এন্ডপয়েন্ট হলো একটা মেথড + পাথের জোড়া, যেমন GET /v1/courses/{course_id}। কাজ অনুযায়ী ভাগ করা Practice API-র এন্ডপয়েন্টগুলো:
| এন্ডপয়েন্ট | যা করে | অথেনটিকেশন |
|---|---|---|
GET /, GET /health | নাম আর ভার্সন; "আমি কি চালু আছি?" | নেই |
GET /v1/courses | কোর্সের তালিকা; কোয়েরি: level, page, per_page | নেই |
GET /v1/courses/{course_id} | একটা কোর্স, অথবা 404 | নেই |
GET /v1/weather?city=… | বাংলাদেশের পাঁচটা শহরের বানানো আবহাওয়া | নেই |
POST /v1/reviews | একটা রিভিউ তৈরি করা | X-API-Key-এ API কি |
POST /v1/token | লগইন করে একটা বেয়ারার টোকেন পাওয়া | বডিতে ইউজারনেম + পাসওয়ার্ড |
GET /v1/me | আমি কে? | বেয়ারার টোকেন |
POST /v1/chat | আসল চ্যাট API-র মতো আকারের একটা নকল LLM | বেয়ারার টোকেন |
GET /v1/limited | রেট লিমিটেড: প্রতি X-Client-Id-এর জন্য ১০ সেকেন্ডে ৩টা রিকোয়েস্ট | নেই |
GET /v1/flaky, GET /v1/slow | ইচ্ছা করে কয়েকবার ব্যর্থ হয়; ইচ্ছা করে ধীরে উত্তর দেয় | নেই |
/v1/echo, GET /v1/status/{code} | আগের পাতার শেখার টুল | নেই |
Practice API সেট আপ করা
Practice API হলো FastAPI দিয়ে লেখা একটা Python ফাইল; FastAPI আপনি শিখবেন আপনার প্রথম FastAPI অ্যাপ থেকে। এখনই এটা বোঝার দরকার নেই; শুধু চালাতে পারলেই হবে। একটা ভার্চুয়াল এনভায়রনমেন্টসহ প্রজেক্ট ফোল্ডার বানান (এনভায়রনমেন্ট ভেরিয়েবল, .env ফাইল ও ভার্চুয়াল এনভায়রনমেন্ট পাতার মতো) আর FastAPI ইনস্টল করুন:
mkdir -p ~/projects/practice-api
cd ~/projects/practice-api
python -m venv .venv
source .venv/bin/activate
pip install -q "fastapi[standard]"Windows PowerShell-এ source …-এর বদলে .venv\Scripts\Activate.ps1 দিয়ে অ্যাক্টিভেট করুন। uv পছন্দ করলে: uv init, তারপর uv add "fastapi[standard]"।
এবার ওই ফোল্ডারে হুবহু এই কনটেন্ট দিয়ে practice_api.py তৈরি করুন (পুরোটা কপি করুন; ফাইলটা সবার জন্য একই, তাই আপনার আউটপুট টিউটোরিয়ালের সাথে মিলবে):
"""BitByte Practice API: a small, offline web API for the Git, APIs and Backend tutorials.
Run it in its own terminal and leave it running:
pip install "fastapi[standard]"
fastapi run practice_api.py --port 8800
Then open http://localhost:8800/docs. Everything it returns is made up and the same
on every machine, so your output matches the tutorial's.
"""
import asyncio
import json
import time
from fastapi import FastAPI, Header, HTTPException, Query, Request, Response
from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import BaseModel, Field
app = FastAPI(title="BitByte Practice API", version="1.0.0")
API_KEY = "practice-key-123"
USERS = {"nadia": "s3cret-pass"}
TOKENS = {"token-nadia-001": "nadia"}
COURSES = [
{"id": 1, "title": "How AI Works", "level": "beginner", "hours": 3, "rating": 4.7},
{"id": 2, "title": "Python for AI", "level": "beginner", "hours": 12, "rating": 4.8},
{"id": 3, "title": "Maths for AI", "level": "beginner", "hours": 8, "rating": 4.5},
{"id": 4, "title": "SQL and Data Handling", "level": "beginner", "hours": 10, "rating": 4.6},
{"id": 5, "title": "Git, APIs and Backend", "level": "beginner", "hours": 9, "rating": 4.6},
{"id": 6, "title": "Prompt Engineering", "level": "intermediate", "hours": 5, "rating": 4.4},
{"id": 7, "title": "LLM APIs", "level": "intermediate", "hours": 6, "rating": 4.7},
{"id": 8, "title": "RAG Systems", "level": "advanced", "hours": 10, "rating": 4.9},
{"id": 9, "title": "AI Agents", "level": "advanced", "hours": 12, "rating": 4.8},
{"id": 10, "title": "LLMOps", "level": "advanced", "hours": 8, "rating": 4.3},
]
WEATHER = {
"dhaka": {"city": "Dhaka", "temp_c": 31.5, "humidity": 78, "condition": "Humid"},
"chattogram": {"city": "Chattogram", "temp_c": 29.8, "humidity": 82, "condition": "Rain"},
"sylhet": {"city": "Sylhet", "temp_c": 27.4, "humidity": 85, "condition": "Rain"},
"khulna": {"city": "Khulna", "temp_c": 32.1, "humidity": 70, "condition": "Sunny"},
"rajshahi": {"city": "Rajshahi", "temp_c": 33.6, "humidity": 58, "condition": "Sunny"},
}
REVIEWS: list[dict] = []
RATE_WINDOW_SECONDS = 10
RATE_LIMIT = 3
_rate_hits: dict[str, list[float]] = {}
_flaky_calls: dict[str, int] = {}
class ReviewIn(BaseModel):
course_id: int = Field(ge=1)
rating: int = Field(ge=1, le=5)
comment: str = Field(min_length=3, max_length=500)
class ChatMessage(BaseModel):
role: str
content: str
class ChatRequest(BaseModel):
model: str = "practice-llm-1"
messages: list[ChatMessage] = Field(min_length=1)
stream: bool = False
def require_api_key(x_api_key: str | None) -> None:
if x_api_key is None:
raise HTTPException(401, "Missing API key: send it in the X-API-Key header")
if x_api_key != API_KEY:
raise HTTPException(403, "Invalid API key")
def require_bearer(authorization: str | None) -> str:
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(401, "Missing bearer token", headers={"WWW-Authenticate": "Bearer"})
user = TOKENS.get(authorization.removeprefix("Bearer "))
if user is None:
raise HTTPException(401, "Invalid or expired token", headers={"WWW-Authenticate": "Bearer"})
return user
@app.get("/")
def home():
return {"name": "BitByte Practice API", "version": "1.0.0", "docs": "/docs"}
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/v1/courses")
def list_courses(
level: str | None = None,
page: int = Query(1, ge=1),
per_page: int = Query(5, ge=1, le=20),
):
items = [c for c in COURSES if level is None or c["level"] == level]
start = (page - 1) * per_page
chunk = items[start:start + per_page]
return {
"page": page,
"per_page": per_page,
"total": len(items),
"next_page": page + 1 if start + per_page < len(items) else None,
"items": chunk,
}
@app.get("/v1/courses/{course_id}")
def get_course(course_id: int):
for course in COURSES:
if course["id"] == course_id:
return course
raise HTTPException(404, f"Course {course_id} not found")
@app.get("/v1/weather")
def weather(city: str):
data = WEATHER.get(city.lower())
if data is None:
raise HTTPException(404, f"No weather data for {city!r}")
return data
@app.post("/v1/reviews", status_code=201)
def create_review(review: ReviewIn, x_api_key: str | None = Header(None)):
require_api_key(x_api_key)
if not any(c["id"] == review.course_id for c in COURSES):
raise HTTPException(404, f"Course {review.course_id} not found")
saved = {"id": len(REVIEWS) + 1, **review.model_dump()}
REVIEWS.append(saved)
return saved
@app.post("/v1/token")
def token(form: dict):
username, password = form.get("username"), form.get("password")
if USERS.get(username) != password:
raise HTTPException(401, "Wrong username or password")
return {"access_token": "token-nadia-001", "token_type": "bearer", "expires_in": 3600}
@app.get("/v1/me")
def me(authorization: str | None = Header(None)):
user = require_bearer(authorization)
return {"username": user, "plan": "student"}
@app.get("/v1/limited")
def limited(x_client_id: str = Header("anonymous")):
"""Allows 3 requests per 10 seconds per X-Client-Id, then answers 429."""
now = time.monotonic()
hits = [t for t in _rate_hits.get(x_client_id, []) if now - t < RATE_WINDOW_SECONDS]
if len(hits) >= RATE_LIMIT:
retry_after = max(1, int(RATE_WINDOW_SECONDS - (now - hits[0])) + 1)
_rate_hits[x_client_id] = hits
return JSONResponse(
{"detail": "Too many requests"}, status_code=429, headers={"Retry-After": str(retry_after)}
)
hits.append(now)
_rate_hits[x_client_id] = hits
return {"ok": True, "remaining": RATE_LIMIT - len(hits)}
@app.get("/v1/flaky")
def flaky(key: str, fail_times: int = Query(2, ge=0, le=10)):
"""Fails with 503 for the first `fail_times` calls with this key, then succeeds."""
calls = _flaky_calls.get(key, 0) + 1
_flaky_calls[key] = calls
if calls <= fail_times:
return JSONResponse({"detail": "Service temporarily unavailable"}, status_code=503)
return {"ok": True, "attempt": calls}
@app.get("/v1/slow")
async def slow(seconds: float = Query(2.0, ge=0, le=30)):
await asyncio.sleep(seconds)
return {"waited_seconds": seconds}
@app.api_route("/v1/echo", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
async def echo(request: Request):
body = await request.body()
try:
parsed = json.loads(body) if body else None
except json.JSONDecodeError:
parsed = body.decode(errors="replace")
shown = {k: v for k, v in request.headers.items() if k in ("content-type", "authorization", "x-api-key", "user-agent", "accept")}
return {"method": request.method, "query": dict(request.query_params), "headers": shown, "json": parsed}
@app.get("/v1/status/{code}")
def status(code: int):
if not 200 <= code <= 599:
raise HTTPException(400, "Status code must be between 200 and 599")
return Response(status_code=code) if code in (204, 304) else JSONResponse({"status": code}, status_code=code)
def _fake_answer(messages: list[ChatMessage]) -> str:
question = messages[-1].content.strip().rstrip("?")
return f"This is a practice answer about: {question}. A real model would say more."
@app.post("/v1/chat")
async def chat(request: ChatRequest, authorization: str | None = Header(None)):
"""A fake LLM. Same shape as many real chat APIs; the answer is always predictable."""
require_bearer(authorization)
answer = _fake_answer(request.messages)
prompt_tokens = sum(len(m.content.split()) for m in request.messages)
completion_tokens = len(answer.split())
usage = {"prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens,
"total_tokens": prompt_tokens + completion_tokens}
if not request.stream:
return {"model": request.model, "message": {"role": "assistant", "content": answer}, "usage": usage}
async def events():
for word in answer.split(" "):
yield f"data: {json.dumps({'delta': word + ' '})}\n\n"
await asyncio.sleep(0.05)
yield f"data: {json.dumps({'done': True, 'usage': usage})}\n\n"
return StreamingResponse(events(), media_type="text/event-stream")ফাইলটা যাতে রহস্যময় বাক্স না থাকে, তাই এক ঝলকে ঘুরে দেখা: COURSES আর WEATHER হলো বানানো ডেটা; যে ফাংশনের ওপরে @app.get(…) বা @app.post(…) আছে, সেটা একটা এন্ডপয়েন্ট; HTTPException(404, …) দিয়ে এটা এরর স্ট্যাটাসসহ উত্তর দেয়; require_api_key আর require_bearer ক্রেডেনশিয়াল যাচাই করে। সবকিছু মেমরিতে থাকে, তাই সার্ভার রিস্টার্ট করলে রিভিউ আর রেট লিমিট আবার শুরু থেকে হয়।
একটা দ্বিতীয় টার্মিনাল খুলুন (সার্ভারের নিজের একটা টার্মিনাল লাগে, আর সেটা চলতেই থাকে), ফোল্ডারে যান, এনভায়রনমেন্ট অ্যাক্টিভেট করুন আর সার্ভার চালু করুন:
# দ্বিতীয় টার্মিনাল: এটা চালু রেখে দিন
cd ~/projects/practice-api
source .venv/bin/activate
fastapi run practice_api.py --port 8800FastAPI একটা ছোট ব্যানার প্রিন্ট করে, আর শেষ লাইনে লেখা থাকে Uvicorn running on http://0.0.0.0:8800 (Press CTRL+C to quit)। 0.0.0.0 মানে "এই কম্পিউটারের প্রতিটা নেটওয়ার্ক ইন্টারফেসে শোনো"; আপনি এতে কানেক্ট করবেন localhost হিসেবে। এখন থেকে ওই টার্মিনাল প্রতিটা রিকোয়েস্টের জন্য একটা লগ লাইন দেখাবে, যেমন "GET /health HTTP/1.1" 200 OK; আপনার রিকোয়েস্ট পৌঁছাচ্ছে কি না দেখার এটা একটা সহজ উপায়। সার্ভার থামাতে Ctrl+C; কোনো পাতায় দরকার হলে একইভাবে আবার চালু করুন।
প্রথম টার্মিনালে ফিরে এসে পরীক্ষা করুন:
curl -s http://localhost:8800/health{"status":"ok"}এখানে "Connection refused" মানে সার্ভার চলছে না (বা অন্য পোর্টে চলছে)। দ্বিতীয় টার্মিনালটা দেখুন: সার্ভার কি চালু হয়েছে, নাকি "address already in use"-এর মতো কোনো এরর দেখিয়েছে?
API ডকুমেন্টেশন পড়া
ব্রাউজারে http://localhost:8800/docs খুলুন। এটা Swagger UI, কোড থেকে আপনা-আপনি তৈরি। প্রতিটা এন্ডপয়েন্টের তালিকা আছে; একটায় ক্লিক করলে দেখবেন এর প্যারামিটার, এটা কেমন রিকোয়েস্ট বডি চায়, আর রেসপন্সের উদাহরণ। Try it out → মান বসান → Execute চাপলে একটা আসল রিকোয়েস্ট যায়, আর রেসপন্স, স্ট্যাটাস কোড ও সমতুল্য curl কমান্ড দেখায়।
পাতাটা আঁকা হয় একটা মেশিনে-পড়ার-উপযোগী বর্ণনা থেকে, যার নাম OpenAPI স্পেসিফিকেশন, ঠিকানা /openapi.json। REST API বর্ণনা করার স্ট্যান্ডার্ড উপায় হলো OpenAPI; বেশিরভাগ গুরুত্বপূর্ণ API একটা প্রকাশ করে, আর টুলগুলো এটা থেকে ক্লায়েন্ট কোড, টেস্ট আর ডকুমেন্টেশন তৈরি করে। যেহেতু এটা JSON, একটা প্রোগ্রাম দিয়েই পড়া যায়:
curl -s http://localhost:8800/openapi.json | python -c '
import json, sys
spec = json.load(sys.stdin)
print("OpenAPI", spec["openapi"], "|", spec["info"]["title"], spec["info"]["version"])
for path, operations in spec["paths"].items():
print(f"{path:26}", ", ".join(sorted(m.upper() for m in operations)))
'OpenAPI 3.1.0 | BitByte Practice API 1.0.0
/ GET
/health GET
/v1/courses GET
/v1/courses/{course_id} GET
/v1/weather GET
/v1/reviews POST
/v1/token POST
/v1/me GET
/v1/limited GET
/v1/flaky GET
/v1/slow GET
/v1/echo DELETE, GET, PATCH, POST, PUT
/v1/status/{code} GET
/v1/chat POST
নতুন একটা API পেলে এর ডকুমেন্টেশন এই ক্রমে পড়ুন। Practice API, GitHub আর যেকোনো LLM প্রোভাইডার, সবার ক্ষেত্রেই এটা কাজ করে:
| প্রশ্ন | কোথায় দেখবেন | Practice API |
|---|---|---|
| বেস URL কী? | "Getting started" / "API reference"-এর শুরু | http://localhost:8800 |
| অথেনটিকেট করব কীভাবে? | "Authentication" পাতা | X-API-Key হেডার বা Authorization: Bearer … |
| কোন এন্ডপয়েন্ট আমার কাজটা করে? | এন্ডপয়েন্টের তালিকা | যেমন GET /v1/courses |
| কোন প্যারামিটার, কোথায় (পাথ, কোয়েরি, বডি), বাধ্যতামূলক নাকি ঐচ্ছিক? | এন্ডপয়েন্টের পাতা / স্কিমা | city: কোয়েরি, বাধ্যতামূলক |
| সফল হলে দেখতে কেমন? কী কী এরর হতে পারে? | রেসপন্সের উদাহরণ, "Errors" পাতা | 200 + JSON; 401/403/404/422/429 |
| সীমা আর দাম কত? | "Rate limits", "Pricing", "Usage" | /v1/limited-এ ১০ সেকেন্ডে ৩টা রিকোয়েস্ট |
| ভার্সন আর পরিবর্তন কীভাবে চলে? | "Versioning", "Changelog" | পাথে /v1/ |
বেস URL আর ভার্সন
প্রতিটা এন্ডপয়েন্ট একটা বেস URL-এর সাপেক্ষে: এখানে http://localhost:8800, বড় LLM প্রোভাইডারদের জন্য https://api.openai.com/v1 বা https://api.anthropic.com/v1, GitHub-এর জন্য https://api.github.com। বেস URL এক জায়গায় রাখুন (একটা কনস্ট্যান্ট বা এনভায়রনমেন্ট ভেরিয়েবলে), যাতে একই কোডকে একটা টেস্ট সার্ভার, স্টেজিং কপি বা অন্য প্রোভাইডারের দিকে ঘোরাতে পারেন।
API বদলায়, অথচ গত বছর লেখা ক্লায়েন্টকেও চলতে হবে। তাই API-র ভার্সন থাকে। দুটো প্রচলিত ধরন: পাথে ভার্সন (/v1/courses; OpenAI, Anthropic আর Practice API এভাবে করে), অথবা হেডারে ভার্সন (Anthropic এর সাথে anthropic-version: 2023-06-01-ও চায়; GitHub ব্যবহার করে X-GitHub-Api-Version)। একটা ভার্সনের ভেতরে প্রোভাইডাররা শুধু নতুন জিনিস যোগ করে (নতুন ঐচ্ছিক ফিল্ড, নতুন এন্ডপয়েন্ট); ভেঙে দেওয়ার মতো পরিবর্তন আসে নতুন ভার্সনে, একটা বাতিলের সময়সীমাসহ। এ কারণেই আপনার JSON-পড়ার কোডের উচিত অচেনা ফিল্ড উপেক্ষা করা, সেগুলোর জন্য ব্যর্থ না হওয়া।
JSON রেসপন্স: একটা আইটেম আর তালিকা
একটা রিসোর্স ফেরত আসে একটা অবজেক্ট হিসেবে। তালিকা সাধারণত খালি অ্যারে হিসেবে নয়, পাতার তথ্যসহ একটা অবজেক্টে মোড়া অবস্থায় আসে, যাতে API পরে আরও ফিল্ড যোগ করতে পারে:
curl -s "http://localhost:8800/v1/courses?per_page=3" | python -m json.tool{
"page": 1,
"per_page": 3,
"total": 10,
"next_page": 2,
"items": [
{
"id": 1,
"title": "How AI Works",
"level": "beginner",
"hours": 3,
"rating": 4.7
},
{
"id": 2,
"title": "Python for AI",
"level": "beginner",
"hours": 12,
"rating": 4.8
},
{
"id": 3,
"title": "Maths for AI",
"level": "beginner",
"hours": 8,
"rating": 4.5
}
]
}এটাই পেজিনেশন (pagination): কোর্স আছে ১০টা, আপনি পেয়েছেন ৩টা, আর next_page বলছে এরপর কোন পাতা চাইতে হবে (শেষ পাতায় এটা null হয়ে যায়)। কখনো ধরে নেবেন না যে একটা রিকোয়েস্টেই সব আসবে; টিউটোরিয়ালের পরের পাতায় আপনি next_page ধরে এগোনো একটা লুপ লিখবেন।
পাবলিক আর অথেনটিকেটেড এন্ডপয়েন্ট
/v1/courses পাবলিক: যে কেউ কল করতে পারে। রিভিউ তৈরি করা তা নয়। আসল API প্রায় প্রতিটা কলেই পরিচয় যাচাই করে, একদিকে ডেটা রক্ষা করতে, অন্যদিকে প্রতিটা কলে তাদের টাকা খরচ হয় বলে। যা দেখবেন তার বেশিরভাগই দুটো পদ্ধতির মধ্যে পড়ে।
API কি
API কি হলো একটা লম্বা গোপন স্ট্রিং, যা আপনার অ্যাকাউন্ট বা প্রজেক্টকে চিহ্নিত করে। এটা আপনি প্রোভাইডারের ড্যাশবোর্ডে তৈরি করেন আর প্রতিটা রিকোয়েস্টের সাথে একটা হেডারে পাঠান। সফল হওয়ার আগে POST /v1/reviews-এ একটা রিকোয়েস্ট যে তিনভাবে ব্যর্থ হতে পারে, দেখুন:
curl -s -w " -> %{http_code}\n" http://localhost:8800/v1/reviews \
--json '{"course_id": 2, "rating": 5, "comment": "Clear and practical"}'
curl -s -w " -> %{http_code}\n" http://localhost:8800/v1/reviews -H "X-API-Key: wrong-key" \
--json '{"course_id": 2, "rating": 5, "comment": "Clear and practical"}'
curl -s -w " -> %{http_code}\n" http://localhost:8800/v1/reviews -H "X-API-Key: practice-key-123" \
--json '{"course_id": 2, "rating": 9, "comment": "ok"}'{"detail":"Missing API key: send it in the X-API-Key header"} -> 401
{"detail":"Invalid API key"} -> 403
{"detail":[{"type":"less_than_equal","loc":["body","rating"],"msg":"Input should be less than or equal to 5","input":9,"ctx":{"le":5}},{"type":"string_too_short","loc":["body","comment"],"msg":"String should have at least 3 characters","input":"ok","ctx":{"min_length":3}}]} -> 422- 401: কোনো কি-ই নেই, "আপনি কে?"।
- 403: কি আছে, কিন্তু বৈধ নয়। (এখানে API-ভেদে পার্থক্য আছে: অনেকে ভুল কি-র জন্যও 401 দেয়। শুধু কোড নয়, এররের বডি পড়ুন।)
- 422: কি ঠিক আছে; ডেটা ঠিক নেই। দুটো সমস্যাই তালিকায় আছে, প্রতিটার অবস্থান (
body→rating) আর কোন নিয়ম ভেঙেছে সেটাসহ।
আসল কি কখনো কমান্ডে টাইপ করবেন না: সেটা আপনার শেল হিস্ট্রি আর স্ক্রিনশটে চলে যায়। এটা একটা এনভায়রনমেন্ট ভেরিয়েবলে রাখুন (অথবা Git উপেক্ষা করে এমন একটা .env ফাইলে, .gitignore: সিক্রেট আর অপ্রয়োজনীয় ফাইল বাইরে রাখা পাতার মতো) আর ভেরিয়েবলটা উল্লেখ করুন:
export PRACTICE_API_KEY=practice-key-123
curl -s -i http://localhost:8800/v1/reviews -H "X-API-Key: $PRACTICE_API_KEY" \
--json '{"course_id": 2, "rating": 5, "comment": "Clear and practical"}' | grep -v '^date' | tr -d '\r'HTTP/1.1 201 Created
server: uvicorn
content-length: 65
content-type: application/json
{"id":1,"course_id":2,"rating":5,"comment":"Clear and practical"}201 Created, আর রেসপন্সে আছে নতুন রিভিউটা, সার্ভার যে id দিয়েছে সেটাসহ। (প্র্যাকটিস কি গোপন নয়, কিন্তু আসল ব্যাপার অভ্যাসটা: একটা আসল LLM কি ফাঁস হলে প্রতিটা রিকোয়েস্টের বিল আপনার নামে হয়।)
বেয়ারার টোকেন
দ্বিতীয় পদ্ধতি: আপনি একবার লগইন করেন, সার্ভার আপনাকে একটা টোকেন দেয়, আর আপনি সেই টোকেন স্ট্যান্ডার্ড Authorization হেডারে Bearer <token> হিসেবে পাঠান ("এই টোকেন যে বহন করছে")। টোকেনের সাধারণত মেয়াদ থাকে (expires_in সেকেন্ডে), তাই চুরি হওয়া টোকেন কিছু সময়ের জন্যই কাজে লাগে। অনেক LLM প্রোভাইডার API কি-র জন্যও একই হেডার ব্যবহার করে: OpenAI চায় Authorization: Bearer sk-…।
curl -s http://localhost:8800/v1/token --json '{"username": "nadia", "password": "s3cret-pass"}'{"access_token":"token-nadia-001","token_type":"bearer","expires_in":3600}টোকেন ছাড়া /v1/me প্রত্যাখ্যান করে, আর www-authenticate হেডার বলে দেয় সে কোন পদ্ধতি চায়:
curl -si http://localhost:8800/v1/me | grep -v '^date' | tr -d '\r'HTTP/1.1 401 Unauthorized
server: uvicorn
www-authenticate: Bearer
content-length: 33
content-type: application/json
{"detail":"Missing bearer token"}সব একসাথে: লগইন করুন, এক লাইনের একটা Python প্রোগ্রাম দিয়ে JSON থেকে টোকেনটা বের করুন, একটা শেল ভেরিয়েবলে রাখুন, আর ব্যবহার করুন:
TOKEN=$(curl -s http://localhost:8800/v1/token \
--json '{"username": "nadia", "password": "s3cret-pass"}' \
| python -c 'import json, sys; print(json.load(sys.stdin)["access_token"])')
curl -s http://localhost:8800/v1/me -H "Authorization: Bearer $TOKEN"{"username":"nadia","plan":"student"}এই লগইন ইচ্ছা করেই সরল রাখা হয়েছে। আসল "Google/GitHub দিয়ে লগইন" ফ্লো (PKCE-সহ OAuth 2.0) আর নিজের API-তে কীভাবে নিরাপদে লগইন বানাবেন, তা আছে অ্যাডভান্সড টিউটোরিয়ালে; ক্লায়েন্ট হিসেবে আপনি যে অংশটা ব্যবহার করেন, Authorization: Bearer …, সেটা হুবহু এমনই থাকে।
প্রচলিত API এরর আর করণীয়
| যা দেখছেন | সম্ভাব্য কারণ | যা করবেন |
|---|---|---|
| curl: "Couldn't connect to server" / "Connection refused" | সার্ভার চলছে না, ভুল হোস্ট বা পোর্ট | URL দেখুন; সার্ভার চালু করুন; ফায়ারওয়াল/VPN দেখুন |
401 Unauthorized | কি/টোকেন নেই, ভুল টাইপ, বা মেয়াদোত্তীর্ণ; ভুল হেডারের নাম | ভেরিয়েবলটা (আংশিক) প্রিন্ট করুন; X-API-Key নাকি Authorization: Bearer দেখুন; আবার লগইন করুন |
403 Forbidden | পরিচয় বৈধ, অনুমতি নেই (প্ল্যান, স্কোপ, প্রজেক্ট) | ড্যাশবোর্ডে কি-র অনুমতি বা প্ল্যান দেখুন |
404 Not Found | ভুল পাথ, ভুল আইডি, /v1 বাদ পড়েছে, অচেনা মডেলের নাম | ডকুমেন্টেশনের সাথে URL অক্ষরে অক্ষরে মেলান |
405 Method Not Allowed | পাথ ঠিক, মেথড ভুল | ডকুমেন্টেশনে মেথড দেখুন (POST নাকি GET) |
422 / 400 | বডি বা প্যারামিটার অবৈধ: ফিল্ড নেই, ভুল টাইপ, সীমার বাইরে | এররের বডি পড়ুন; ফিল্ডের নাম ওখানে আছে |
429 Too Many Requests | রেট লিমিট বা কোটা | Retry-After সেকেন্ড অপেক্ষা করুন, গতি কমান, রিট্রাই করুন; কোটা দেখুন |
5xx | সার্ভার বা প্রোভাইডারের সমস্যা | অপেক্ষা করে রিট্রাই করুন; প্রোভাইডারের স্ট্যাটাস পেজ দেখুন |
| টাইমআউট | ধীর সার্ভার বা নেটওয়ার্ক; খুব লম্বা LLM জেনারেশন | সবসময় টাইমআউট দিন; আইডেমপোটেন্ট রিকোয়েস্ট রিট্রাই করুন; লম্বা জেনারেশন স্ট্রিম করুন |
curl গভীরভাবে
curl-এর গুরুত্বপূর্ণ অপশনগুলোর বেশিরভাগ আপনি এর মধ্যেই ব্যবহার করেছেন। এখানে সব একসাথে:
| অপশন | কাজ |
|---|---|
-s / -S | নীরব (প্রগ্রেস মিটার নেই) / তবু এরর দেখাও। স্ক্রিপ্টে -sS একটা ভালো ডিফল্ট। |
-i / -v | রেসপন্সের স্ট্যাটাস লাইন আর হেডার সাথে দেখাও / পুরো কথোপকথন দেখাও। |
-X METHOD | মেথড বাছাই (-X DELETE)। |
-H "Name: value" | একটা হেডার যোগ করা। আরও দরকার হলে বারবার দিন। |
-d DATA / --json DATA | একটা বডি পাঠানো (ফর্ম হিসেবে) / ঠিক হেডারসহ JSON পাঠানো। -d @file.json বডিটা ফাইল থেকে পড়ে। |
-G + --data-urlencode | এনকোড করা মান কোয়েরি স্ট্রিংয়ে বসানো। |
-F "file=@notes.pdf" | মাল্টিপার্ট আপলোড। |
-o FILE / -O | বডি একটা ফাইলে সেভ করা / URL-এর নামে একটা ফাইলে সেভ করা। |
-L | রিডাইরেক্ট (3xx) অনুসরণ করা। |
-w FORMAT | ট্রান্সফারের পরে খুঁটিনাটি প্রিন্ট করা: %{http_code}, %{content_type}, %{time_total}। |
--fail-with-body | 4xx/5xx হলে কোড 22 দিয়ে বের হওয়া (বডি তবু প্রিন্ট করে), যাতে স্ক্রিপ্ট ব্যর্থতা টের পায়। |
-m SECONDS | মোট এত সেকেন্ড পরে হাল ছেড়ে দেওয়া। |
--retry N | সাময়িক ব্যর্থতায় (429 আর 503-সহ) সর্বোচ্চ N বার রিট্রাই। |
এক্সিট কোড: ব্যর্থতাকে স্ক্রিপ্টের চোখে দৃশ্যমান করা
ডিফল্টভাবে curl যেকোনো HTTP রেসপন্স পেলেই, এমনকি 404 পেলেও, 0 ("সফল") দিয়ে বের হয়, কারণ curl-এর দৃষ্টিতে ট্রান্সফার তো হয়েছে। স্ক্রিপ্টে এতে ব্যর্থতা চাপা পড়ে। $?-এ থাকে শেষ কমান্ডের এক্সিট কোড:
curl -s http://localhost:8800/v1/courses/99 > /dev/null
echo "without --fail-with-body: exit code $?"
curl -s --fail-with-body http://localhost:8800/v1/courses/99
echo " with --fail-with-body: exit code $?"without --fail-with-body: exit code 0
{"detail":"Course 99 not found"} with --fail-with-body: exit code 22এক্সিট কোড 22 মানে "সার্ভার একটা HTTP এরর দিয়েছে"। আরেকটা যা দেখবেন তা হলো 28, "টাইমআউট"। /v1/slow উত্তর দেওয়ার আগে আপনি যতক্ষণ চান ততক্ষণ অপেক্ষা করে; curl-কে মাত্র এক সেকেন্ড দিন:
curl -s -m 1 "http://localhost:8800/v1/slow?seconds=3"
echo "exit code $?"exit code 28টাইমআউট কোনো স্ট্যাটাস কোড নয়: কোনো রেসপন্সই আসেনি। API কল করে এমন প্রতিটা প্রোগ্রামে একটা টাইমআউট দেওয়া উচিত। ওয়েব সার্ভারে একটা আটকে থাকা রিকোয়েস্ট একটা ওয়ার্কারকে জিম্মি করে রাখে; ডেটা পাইপলাইনে এটা একটা জবকে ঘণ্টার পর ঘণ্টা থামিয়ে রাখতে পারে।
রেসপন্স সেভ করা
-o বডিটা একটা ফাইলে লেখে, আর -w তবু জানাতে পারে কেমন হলো। টেস্টের জন্য বা pandas দিয়ে ঘেঁটে দেখার জন্য কোডের পাশে একটা নমুনা রেসপন্স রাখার উপায় এটাই:
curl -s -o dhaka.json -w "%{http_code} %{content_type}\n" "http://localhost:8800/v1/weather?city=Dhaka"
python -m json.tool dhaka.json200 application/json
{
"city": "Dhaka",
"temp_c": 31.5,
"humidity": 78,
"condition": "Humid"
}ব্যবহারের সীমা
প্রতিটা আসল API সীমা বেঁধে দেয় আপনি কতটা ব্যবহার করতে পারবেন: প্রতি মিনিটে এতগুলো রিকোয়েস্ট (RPM), আর LLM-এর ক্ষেত্রে প্রতি মিনিটে এতগুলো টোকেন (TPM) আর একটা খরচের বাজেটও। সীমাগুলো সার্ভিসকে অতিরিক্ত চাপ থেকে বাঁচায়, আর একজন ক্লায়েন্ট যেন বাকিদের বঞ্চিত না করে তা নিশ্চিত করে। সীমা পেরোলে পাবেন 429 Too Many Requests, সাধারণত একটা Retry-After হেডারসহ, যা বলে কত সেকেন্ড অপেক্ষা করতে হবে। LLM প্রোভাইডাররা এমন হেডারও পাঠায়, যা দেখায় আপনার সীমার কতটা বাকি; ভালো ক্লায়েন্ট সেগুলোর দিকে নজর রাখে।
Practice API-র /v1/limited প্রতিটা ক্লায়েন্টকে (X-Client-Id হেডার দিয়ে চেনা) ১০ সেকেন্ডে ৩টা রিকোয়েস্ট করতে দেয়। দ্রুত চারটা পাঠান:
for i in 1 2 3 4; do
curl -s -w " -> %{http_code}\n" -H "X-Client-Id: nadia-laptop" http://localhost:8800/v1/limited
done
curl -si -H "X-Client-Id: nadia-laptop" http://localhost:8800/v1/limited | grep -i -E '^HTTP|^retry-after' | tr -d '\r'{"ok":true,"remaining":2} -> 200
{"ok":true,"remaining":1} -> 200
{"ok":true,"remaining":0} -> 200
{"detail":"Too many requests"} -> 429
HTTP/1.1 429 Too Many Requests
retry-after: 10চতুর্থ আর পঞ্চম রিকোয়েস্ট প্রত্যাখ্যাত হয়েছে, আর সার্ভার বলছে: ১০ সেকেন্ড পরে আবার চেষ্টা করুন। অন্য একজন ক্লায়েন্টের নিজস্ব বরাদ্দ আছে:
curl -s -H "X-Client-Id: rafi-laptop" http://localhost:8800/v1/limited{"ok":true,"remaining":2}429-এর সঠিক জবাব হলো অপেক্ষা করে রিট্রাই করা, সাথে সাথে একটা আঁটসাঁট লুপে রিট্রাই করা নয় (তাতে ব্লকটা শুধু দীর্ঘ হয়, আর কিছু API-তে আপনার কি সাসপেন্ড হয়ে যায়)। দুই পাতা পরে Python-এ আপনি এমন একটা রিট্রাই ফাংশন লিখবেন, যা Retry-After পড়ে।
গ্রাফিক্যাল (GUI) API ক্লায়েন্ট
curl সবখানে আছে আর স্ক্রিপ্টে ব্যবহার করা যায়, এ কারণেই এই টিউটোরিয়াল এটা ব্যবহার করে। একটা বড় API হাতে-কলমে ঘেঁটে দেখতে অনেক ডেভেলপার একটা GUI বা আরও বন্ধুত্বপূর্ণ CLI-ও ব্যবহার করেন:
| টুল | কী | যা জানা ভালো |
|---|---|---|
Swagger UI (/docs) | OpenAPI থেকে তৈরি ইন্টারঅ্যাক্টিভ ডকুমেন্টেশন | FastAPI-র সাথে বিনা খরচে আসে; আপনার নিজের API-র জন্য একদম উপযুক্ত। |
| Bruno | ওপেন-সোর্স ডেস্কটপ API ক্লায়েন্ট | রিকোয়েস্টগুলো আপনার রিপোজিটরিতে সাধারণ ফাইল হিসেবে রাখে, ফলে কোডের মতোই Git আর কোড রিভিউয়ের ভেতর দিয়ে যায়। |
| Postman | সবচেয়ে পরিচিত API প্ল্যাটফর্ম | কালেকশন, এনভায়রনমেন্ট, টিমে শেয়ার করা; ক্লাউড ফিচারের জন্য অ্যাকাউন্ট লাগে। |
| HTTPie | আরও বন্ধুত্বপূর্ণ একটা কমান্ড-লাইন ক্লায়েন্ট (http :8800/health) | রঙিন, পড়তে সহজ আউটপুট; ডিফল্টভাবে JSON। |
| এডিটর এক্সটেনশন | যেমন VS Code-এর "REST Client" | কোডের পাশে একটা .http ফাইলে রিকোয়েস্ট লিখে ক্লিক করে পাঠান। |
যেটাই বাছুন, কি রাখুন টুলের এনভায়রনমেন্ট/সিক্রেট সেটিংসে, এমন সেভ করা রিকোয়েস্টের ভেতরে নয় যা আপনি কমিট বা শেয়ার করেন।
কোথায় কাজে লাগবে
- প্রথমবার কোনো LLM API চেষ্টা করা। একটা কি কাজ করছে কি না দেখার সবচেয়ে দ্রুত উপায় হলো প্রোভাইডারের ডকুমেন্টেশন থেকে একটা curl কমান্ড, কি-টা একটা এনভায়রনমেন্ট ভেরিয়েবলে রেখে। curl কাজ করলে আর আপনার Python না করলে সমস্যা আপনার কোডে, অ্যাকাউন্টে নয়।
- প্রোভাইডারের ডকুমেন্টেশন পড়া। বেস URL, অথেনটিকেশন হেডার, এন্ডপয়েন্ট, বডির স্কিমা, এরর, রেট লিমিট, ভার্সন: একই চেকলিস্ট খাটে OpenAI, Anthropic, Hugging Face, একটা ভেক্টর ডেটাবেস বা একটা পেমেন্ট API-তে।
- নিজের API-র ডকুমেন্টেশন। FastAPI আপনার ব্যাকএন্ডকে বিনা খরচে একই
/docsআর/openapi.jsonদেবে, যা আপনার ফ্রন্টএন্ড সহকর্মীরা আর আপনার টেস্ট ব্যবহার করবে। - বাগ রিপোর্ট। ব্যর্থ একটা রিকোয়েস্টকে ছোট করে একটা curl কমান্ডে আনা (কি সরিয়ে) হলো একটা ইস্যুতে দেওয়ার মতো সবচেয়ে কাজের জিনিস।
সাধারণ ভুল
- কমান্ড, URL, স্ক্রিনশট বা কমিটে কি। এনভায়রনমেন্ট ভেরিয়েবল,
-H "X-API-Key: $PRACTICE_API_KEY", আর.gitignore-এ.envব্যবহার করুন। আসল কি ফাঁস হলে আগে ড্যাশবোর্ডে সেটা বাতিল (revoke) করুন, তারপর পরিষ্কার করুন। - ভুল অথেনটিকেশন হেডার।
Bearerছাড়াAuthorization: sk-…, অথবা API যেখানেx-api-keyচায় সেখানেAuthorization-এ কি দিলে এমন 401 আসে যা দেখে মনে হয় কি-টাই খারাপ। হেডারের নাম আর ফরম্যাট ডকুমেন্টেশন থেকে কপি করুন। - curl-এর এক্সিট কোড 0-কে বিশ্বাস করা। 404 আর 500 রেসপন্সেও curl সফল বলে। স্ক্রিপ্টে
--fail-with-bodyব্যবহার করুন (অথবা-w "%{http_code}"দেখুন)। - টাইমআউট নেই। একটা ধীর আপস্ট্রিম রিকোয়েস্ট একটা স্ক্রিপ্টকে চিরকাল আটকে রাখতে পারে। curl-এ
-mআর Python-এtimeout=ব্যবহার করুন, সবসময়। - 429-এর পরে অবিরাম রিকোয়েস্ট। লুপে সাথে সাথে রিট্রাই করলে আপনি ব্লকই থাকেন আর কোটা নষ্ট হয়। অন্তত
Retry-Afterসেকেন্ড অপেক্ষা করুন, আর সামগ্রিক রিকোয়েস্টের গতি কমান।
নিজে চেষ্টা করুন
- সহজ: OpenAPI স্পেক ব্যবহার করে
GET /v1/courses-এর প্যারামিটারগুলো প্রিন্ট করুন (নাম, কোথায় যায়, আর বাধ্যতামূলক কি না)। - মাঝারি:
PRACTICE_API_KEY-এ রাখা API কি দিয়ে কোর্স ৯৯-এর জন্য একটা রিভিউ পোস্ট করুন। কোন স্ট্যাটাস আশা করছেন, আর সেটা 422 কেন নয়? - কঠিন: ছোট একটা bash লুপ লিখুন, যা ক্লায়েন্ট
batch-jobহিসেবে/v1/limited-কে পাঁচবার কল করে, আর প্রতিটা কলের জন্য স্ট্যাটাস কোড প্রিন্ট করে, আর (শুধু 429 হলে)Retry-After-এর মানও।
উত্তর
# 1
curl -s http://localhost:8800/openapi.json | python -c '
import json, sys
spec = json.load(sys.stdin)
for p in spec["paths"]["/v1/courses"]["get"]["parameters"]:
print(p["name"], p["in"], "required" if p.get("required") else "optional")
'level query optional
page query optional
per_page query optional# 2
curl -s -w " -> %{http_code}\n" http://localhost:8800/v1/reviews -H "X-API-Key: $PRACTICE_API_KEY" \
--json '{"course_id": 99, "rating": 4, "comment": "Looking forward to it"}'{"detail":"Course 99 not found"} -> 404বডিটা বৈধ (৯৯ একটা পূর্ণসংখ্যা, ১ বা তার বেশি, তাই ভ্যালিডেশন পাস করে), কিন্তু রিভিউ করার মতো কোর্স ৯৯ নেই, তাই সার্ভার 404 দেয়। ভ্যালিডেশন যাচাই করে ডেটার আকার; 404 আসে ডেটাকে যা আছে তার সাথে মিলিয়ে দেখে।
# 3
for i in 1 2 3 4 5; do
headers=$(curl -s -D - -o /dev/null -H "X-Client-Id: batch-job" http://localhost:8800/v1/limited | tr -d '\r')
code=$(echo "$headers" | head -n 1 | cut -d ' ' -f 2)
if [ "$code" = "429" ]; then
wait=$(echo "$headers" | grep -i '^retry-after:' | cut -d ' ' -f 2)
echo "call $i: $code, retry after $wait s"
else
echo "call $i: $code"
fi
donecall 1: 200
call 2: 200
call 3: 200
call 4: 429, retry after 10 s
call 5: 429, retry after 10 s-D - রেসপন্সের হেডারগুলো স্ট্যান্ডার্ড আউটপুটে লেখে, আর -o /dev/null বডি ফেলে দেয়, তাই headers-এ থাকে শুধু স্ট্যাটাস লাইন আর হেডার। bash-এ এটা করা যায়, কিন্তু খটমটে; পরের পাতায় একই কাজ Python-এ অনেক পরিষ্কারভাবে হবে।
সারসংক্ষেপ
- ওয়েব API হলো HTTP-র ওপর একটা চুক্তি। REST একে সাজায় রিসোর্স (পাথে বিশেষ্য) ঘিরে, আর HTTP মেথডকে ক্রিয়াপদ হিসেবে ব্যবহার করে; একটা এন্ডপয়েন্ট মানে একটা মেথড + পাথ।
- Practice API চালান একটা দ্বিতীয় টার্মিনালে,
fastapi run practice_api.py --port 8800দিয়ে। এর ডকুমেন্টেশন/docs-এ, যা তৈরি হয়/openapi.json-এর OpenAPI স্পেক থেকে। - যেকোনো API-র ডকুমেন্টেশনে খুঁজুন: বেস URL, অথেনটিকেশন, এন্ডপয়েন্ট, প্যারামিটার, রেসপন্স আর এরর, সীমা আর দাম, ভার্সন। ভার্সন থাকে পাথে (
/v1) বা একটা হেডারে। - API কি যায়
X-API-Key-এর মতো একটা হেডারে; টোকেন যায়Authorization: Bearer …-এ। 401 = আপনি কে, 403 = অনুমতি নেই, 422 = খারাপ ডেটা। সিক্রেট রাখুন এনভায়রনমেন্ট ভেরিয়েবলে। - curl-এর মূল অপশন:
-s -i -v -X -H --json -G --data-urlencode -o -w -L -m --fail-with-body। এক্সিট কোড 22 = HTTP এরর, 28 = টাইমআউট। 429 আসেRetry-After-সহ: অপেক্ষা করুন, তারপর রিট্রাই।
এরপর: Python-এর requests দিয়ে API কল। curl দিয়ে যা যা করেছেন, এবার সব Python-এ: প্যারামিটার, হেডার, JSON, এরর, টাইমআউট, এনভায়রনমেন্ট থেকে কি, আর পেজিনেশন।