Jobs
One endpoint for every media model, with all of its settings, polling or webhooks, and file uploads.
Images, video, music, speech, avatars, lip sync, upscaling and edits all run through /v1/jobs. You send the model's own fields, as listed on each model's page under Models.
Start a job
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"}}Get the result
Either poll the job, or give a webhook_url and we call you when it finishes.
Poll
curl https://pinerouter.ai/v1/jobs/task_abc123 -H "Authorization: Bearer $API_KEY"{"code": 200, "msg": "success", "data": {"id": "task_abc123", "model": "z-image", "status": "succeeded",
"outputs": ["https://files.example.com/outputs/abc.png"]}}data.status is queued, running, succeeded or failed. On succeeded, data.outputs lists the links to your files. On failed, data.error says why; a failed job costs nothing. Poll every 5 to 10 seconds; most images finish in under a minute, videos in a few minutes.
import time, requests
BASE = "https://pinerouter.ai"
H = {"Authorization": "Bearer sk-..."}
job = requests.post(f"{BASE}/v1/jobs", headers=H, json={"model": "z-image", "input": {"prompt": "A red paper boat"}}).json()["data"]
while job["status"] not in ("succeeded", "failed"):
time.sleep(5)
job = requests.get(f"{BASE}/v1/jobs/{job['id']}", headers=H).json()["data"]
print(job["outputs"] if job["status"] == "succeeded" else job["error"])Older field names still work: taskId (same as id), state (waiting, queuing, generating, success, fail), resultJson (a JSON string with resultUrls) and failMsg.
Webhook
Add webhook_url (a public https:// address) to the request. When the job succeeds or fails we POST the same job object to it:
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"}, "webhook_url": "https://your.app/hooks/jobs"}'{"type": "job.succeeded", "timestamp": "2026-11-02T10:00:00Z", "data": {"id": "task_abc123", "status": "succeeded", "outputs": ["https://..."]}}- Signed per the Standard Webhooks spec: headers
webhook-id,webhook-timestampandwebhook-signature(v1,<base64>). The signature is HMAC-SHA256 of<webhook-id>.<webhook-timestamp>.<body>with your API key as the secret. - Answer with any
2xxwithin 15 seconds. Otherwise we retry after 1, 5, 15 and 60 minutes, then stop. - Delivery is at least once: use
webhook-idto ignore repeats. - Private and local addresses are refused.
import base64, hashlib, hmac
def verify(body: bytes, headers, api_key: str) -> bool:
msg = f"{headers['webhook-id']}.{headers['webhook-timestamp']}.".encode() + body
sig = base64.b64encode(hmac.new(api_key.encode(), msg, hashlib.sha256).digest()).decode()
return any(hmac.compare_digest(sig, s.split(",", 1)[1]) for s in headers["webhook-signature"].split())Inputs
inputtakes the fields on the model's page. Leave a field out to use its default.- Files go in as
https://links (for exampleimage_url,image_urls,video_url,audio_url). Use any public link, or upload the file first:
curl https://pinerouter.ai/v1/files -H "Authorization: Bearer $API_KEY" -F "file=@photo.png"{"id": "6f1c...", "object": "file", "url": "https://files.example.com/uploads/6f1c....png", "bytes": 48213, "content_type": "image/png", "expires_at": 1793680000}Images up to 10 MB, video and audio up to 50 MB. Uploads are deleted after 1 day.
- Safety filters are always on.
Cost
The model's page shows its prices by setting. A job reserves its expected cost plus 20% when it starts and is charged the real cost when it finishes; a failed job costs nothing. Result links last 7 days.
