cURL / HTTP
Call the API from any language with plain HTTP, including a copy-paste helper for media jobs.
Every endpoint is plain HTTPS with JSON. Send your key as Authorization: Bearer <key>. This works from any language, with no library.
Configure API keys
export API_KEY="sk-..."Text
curl https://test-demo.pinerouter.ai/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"model": "gpt-6-luna", "messages": [{"role": "user", "content": "Name three rivers in Europe."}], "max_tokens": 200}'The answer is in choices[0].message.content. Tools, JSON output and vision use the same request: see Text.
Stream
curl -N https://test-demo.pinerouter.ai/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"model": "gpt-6-luna", "stream": true, "messages": [{"role": "user", "content": "Write a haiku about rivers."}]}'Each event is a line data: {...}, and the stream ends with data: [DONE]. See Streaming.
Images
curl https://test-demo.pinerouter.ai/v1/images/generations \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"model": "z-image", "prompt": "A red paper boat on a calm lake", "size": "1024x1024"}'The call waits for the image; the answer has data[].url. See Images.
Jobs (images, video and all media)
Start a job, then poll it until data.status is succeeded or failed.
curl https://test-demo.pinerouter.ai/v1/jobs \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"model": "z-image", "input": {"prompt": "A red paper boat on a calm lake", "aspect_ratio": "1:1"}}'
# {"code": 200, "msg": "success", "data": {"id": "task_abc123", "status": "queued"}}
curl https://test-demo.pinerouter.ai/v1/jobs/task_abc123 -H "Authorization: Bearer $API_KEY"
# {"code": 200, "msg": "success", "data": {"id": "task_abc123", "status": "succeeded", "outputs": ["https://..."]}}Poll every 5 to 10 seconds, or pass webhook_url to be called when the job ends. Fields per model are on its page under Models. Full guide: Jobs.
Jobs helper
This helper starts a job, polls it every 5 seconds, and downloads the files. Copy it into your project.
# pip install requests
import os, time, requests
BASE = "https://test-demo.pinerouter.ai"
H = {"Authorization": f"Bearer {os.environ['API_KEY']}"}
def run_job(model, input, every=5, max_wait=1800):
r = requests.post(f"{BASE}/v1/jobs", headers=H, json={"model": model, "input": input}, timeout=60)
body = r.json()
if r.status_code != 200 or body.get("code") != 200:
raise RuntimeError(f"start failed ({r.status_code}): {body}")
task = body["data"]["id"]
waited = 0
while waited < max_wait:
time.sleep(every)
waited += every
r = requests.get(f"{BASE}/v1/jobs/{task}", headers=H, timeout=60)
if r.status_code == 429 or r.status_code >= 500:
continue # poll again; never start the job a second time
d = r.json()["data"]
if d["status"] == "succeeded":
return task, d["outputs"]
if d["status"] == "failed":
raise RuntimeError(f"job {task} failed: {d.get('error')}")
raise TimeoutError(f"job {task} still running; poll it later")
def download(urls, folder="."):
paths = []
for i, url in enumerate(urls):
name = os.path.join(folder, f"{i}_{url.split('/')[-1].split('?')[0]}")
with requests.get(url, stream=True, timeout=300) as r, open(name, "wb") as f:
r.raise_for_status()
for chunk in r.iter_content(1 << 20):
f.write(chunk)
paths.append(name)
return paths
task, urls = run_job("z-image", {"prompt": "A red paper boat on a calm lake", "aspect_ratio": "1:1"})
print(download(urls))// Node.js 18 or newer (built-in fetch)
import { writeFile } from "node:fs/promises";
const BASE = "https://test-demo.pinerouter.ai";
const H = { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json" };
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export async function runJob(model: string, input: Record<string, unknown>, { every = 5000, maxWait = 1_800_000 } = {}) {
const r = await fetch(`${BASE}/v1/jobs`, { method: "POST", headers: H, body: JSON.stringify({ model, input }) });
const body = await r.json();
if (r.status !== 200 || body.code !== 200) throw new Error(`start failed (${r.status}): ${JSON.stringify(body)}`);
const task: string = body.data.id;
for (let waited = 0; waited < maxWait; waited += every) {
await sleep(every);
const p = await fetch(`${BASE}/v1/jobs/${task}`, { headers: H });
if (p.status === 429 || p.status >= 500) continue; // poll again; never start the job a second time
const d = (await p.json()).data;
if (d.status === "succeeded") return { task, urls: d.outputs as string[] };
if (d.status === "failed") throw new Error(`job ${task} failed: ${d.error}`);
}
throw new Error(`job ${task} still running; poll it later`);
}
export async function download(urls: string[]) {
const paths: string[] = [];
for (const [i, url] of urls.entries()) {
const name = `${i}_${new URL(url).pathname.split("/").pop()}`;
await writeFile(name, Buffer.from(await (await fetch(url)).arrayBuffer()));
paths.push(name);
}
return paths;
}
const { urls } = await runJob("z-image", { prompt: "A red paper boat on a calm lake", aspect_ratio: "1:1" });
console.log(await download(urls));- Result links last 7 days, so the helper downloads them straight away.
- A job that fails costs nothing. A job still running after the wait is not lost: keep the
idand poll it later. - Use it from any SDK or framework on these pages, for example inside a tool.
Specifics
- Errors come back with an HTTP status and a JSON body. Retry
429and5xxwith backoff; see Errors and Rate limits. - Anthropic's Messages format is at
POST /v1/messages. See Claude messages.
Next
- API reference: every endpoint and field
- Jobs: webhooks, uploads and cost
- OpenAI SDK: the same calls with a client library