PineRouterDocs
GuidesModelsAPI referenceApps
SDKs and frameworks

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://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://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://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://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://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://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://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 id and 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 429 and 5xx with backoff; see Errors and Rate limits.
  • Anthropic's Messages format is at POST /v1/messages. See Claude messages.

Next