The PicassoIA API lets you run PicassoIA's image and video models from your own code:
- You create a prediction for a model.
- You poll the prediction until it finishes.
- You read the output URLs.
- Base URL:
https://api.picassoia.com/v1 - Format: JSON requests and responses, UTF-8.
- Price: API predictions are currently free. They use no credits.
- Who can use it: accounts on the Infinite plan.
Contents
- Authentication
- Quick start
- Models
- Endpoints
- The prediction object
- Polling and ETA
- Canceling
- Errors
- Limits
Authentication
Every request needs an API key in the Authorization header:
Authorization: Bearer pia_sk_…- Creating a key. Create keys on picassoia.com, in your account's API keys section.
- The full key is shown once, when you create it. Store it somewhere safe.
- An account can hold 2 keys at a time. To make room, revoke one.
- Revoking a key. A revoked key stops working at once.
- Keep keys server-side. Anyone with a key can run predictions on your account, so never put one in a web page or a mobile app. The API sends no CORS headers, so browsers cannot call it directly.
- Plan changes. Keys keep existing if your plan changes. Without an Infinite plan, creating predictions answers
403 plan_required. Reading, listing and canceling your predictions keeps working.
Quick start
1. Create a prediction:
curl -s -X POST https://api.picassoia.com/v1/models/picassoia/picassoia-image/predictions \
-H "Authorization: Bearer $PICASSOIA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input": {"prompt": "a lighthouse at sunset, oil painting", "aspect_ratio": "16:9"}}'The answer (201 Created) is a prediction in the starting state:
{
"id": "api_3f9c2b7e8d1a4c6f9e0b1a2c3d4e5f60",
"model": "picassoia/picassoia-image",
"input": {
"prompt": "a lighthouse at sunset, oil painting",
"aspect_ratio": "16:9",
"num_outputs": 1,
"output_format": "jpg",
"output_quality": 80
},
"output": null,
"error": null,
"status": "starting",
"created_at": "2026-10-01T09:30:00.000Z",
"started_at": null,
"completed_at": null,
"metrics": { "predict_time": null },
"urls": {
"get": "https://api.picassoia.com/v1/predictions/api_3f9c2b7e8d1a4c6f9e0b1a2c3d4e5f60",
"cancel": "https://api.picassoia.com/v1/predictions/api_3f9c2b7e8d1a4c6f9e0b1a2c3d4e5f60/cancel"
},
"eta": { "seconds": 12, "next_poll_in_seconds": 6 }
}2. Poll it (wait eta.next_poll_in_seconds between calls) until status is succeeded, failed or canceled:
curl -s https://api.picassoia.com/v1/predictions/api_3f9c2b7e8d1a4c6f9e0b1a2c3d4e5f60 \
-H "Authorization: Bearer $PICASSOIA_API_KEY"3. Read the output:
{
"id": "api_3f9c2b7e8d1a4c6f9e0b1a2c3d4e5f60",
"status": "succeeded",
"output": ["https://…/lighthouse.jpg"],
"metrics": { "predict_time": 6.4 },
"eta": null
}(Other fields are left out here.)
Node.js (fetch)
const API = 'https://api.picassoia.com/v1'
const headers = {
Authorization: `Bearer ${process.env.PICASSOIA_API_KEY}`,
'Content-Type': 'application/json',
}
const sleep = (s) => new Promise((resolve) => setTimeout(resolve, s * 1000))
async function run(model, input) {
const created = await fetch(`${API}/models/${model}/predictions`, {
method: 'POST',
headers,
body: JSON.stringify({ input }),
})
let prediction = await created.json()
if (!created.ok) throw new Error(`${prediction.code}: ${prediction.detail}`)
while (!['succeeded', 'failed', 'canceled'].includes(prediction.status)) {
await sleep(prediction.eta?.next_poll_in_seconds ?? 2)
prediction = await (await fetch(prediction.urls.get, { headers })).json()
}
if (prediction.status !== 'succeeded') throw new Error(prediction.error ?? prediction.status)
return prediction.output
}
const video = await run('picassoia/seedance-2.5-lite', {
prompt: 'a paper boat sailing down a rainy street',
duration: 5,
})
console.log(video) // "https://…/video.mp4"Python (requests)
import os, time, requests
API = "https://api.picassoia.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['PICASSOIA_API_KEY']}"}
def run(model, input):
response = requests.post(f"{API}/models/{model}/predictions", json={"input": input}, headers=HEADERS)
prediction = response.json()
if not response.ok:
raise RuntimeError(f"{prediction['code']}: {prediction['detail']}")
while prediction["status"] not in ("succeeded", "failed", "canceled"):
time.sleep((prediction.get("eta") or {}).get("next_poll_in_seconds", 2))
prediction = requests.get(prediction["urls"]["get"], headers=HEADERS).json()
if prediction["status"] != "succeeded":
raise RuntimeError(prediction["error"] or prediction["status"])
return prediction["output"]
images = run("picassoia/picassoia-image-editor-pro", {
"prompt": "put the cat from image 1 on the sofa from image 2",
"images": ["https://example.com/cat.png", "https://example.com/sofa.jpg"],
})
print(images) # ["https://…/result.webp"]Models
The exact schema of every model is also served as JSON Schema: GET /v1/models/{owner}/{name} returns input_schema (with every field, its limits and defaults) and output_schema.
Images as input. Image fields take either kind of value:
- an https URL that is publicly reachable;
- a data URL:
data:image/png;base64,…, alsoimage/jpegorimage/webp, up to 5 MB each.
The images must be PNG, JPEG or WebP. PicassoIA stores its own copy of each one, so input in the prediction shows that copy's URL. Use URLs for anything larger than a few hundred KB: the whole request body is limited to 10 MB.
Unknown fields are refused, so a typo does not silently fall back to a default. The answer is 422 invalid_input.
picassoia/picassoia-image
Output: a list of 1 or 2 image URLs.
picassoia/picassoia-image-editor-pro
Output: a list of 1 or 2 image URLs.
picassoia/picassoia-video
Output: one MP4 URL (a string).
picassoia/seedance-2.5-lite
Output: one MP4 URL (a string).
Endpoints
Model
{
"id": "picassoia/seedance-2.5-lite",
"owner": "picassoia",
"name": "seedance-2.5-lite",
"title": "Seedance 2.5 Lite",
"description": "Text or image to video with synchronized audio: …",
"input_schema": { "type": "object", "properties": { "…": {} }, "required": ["prompt"], "additionalProperties": false },
"output_schema": { "type": "string", "format": "uri" }
}Creating a prediction
POST /v1/models/{owner}/{name}/predictions with the body { "input": { … } }.
- The API answers once the model has accepted the job, usually within seconds, with the prediction in the
startingstate. It never waits for the result. - If the prediction fails right away, the answer is still
201, with the prediction in thefailedstate and itserror. This happens, for example, when the input is refused by the safety filter. - If no prediction was created, the answer is an error: invalid input, plan, too many predictions in progress, service unavailable.
Listing predictions
GET /v1/predictions returns your API predictions, 50 per page, newest first, across all your keys.
nextis the URL of the next page (…/v1/predictions?cursor=…), ornullon the last page.previousis alwaysnull.- Predictions you deleted on picassoia.com are left out.
A prediction is visible through any of your keys, and never to anyone else.
The prediction object
Statuses.
starting versus processing is an estimate, made from the queue position at creation. A new prediction is always starting. The last three statuses are final: a prediction in one of them never changes again.
Errors of a failed prediction. The error of a failed prediction is one of these messages:
- The input or the output was refused by the safety filter. The message ends in
(SAFETY_CHECKER_PICASSO_FILTER_S4). The prediction failed. You can try again.The prediction timed out. You can try again.Nothing finished it within 3 hours.
Outputs. Output URLs are hosted by PicassoIA. Download the files you want to keep.
Polling and ETA
eta.next_poll_in_seconds tells you when the next poll is worth making:
- At half the estimated time.
- When the estimate is reached.
- Then every 2 seconds.
Example with an estimate of 10 s: poll after 5 s, then after 10 s, then every 2 s. Polling faster gets you nothing sooner.
Canceling
POST /v1/predictions/{id}/cancel:
A canceled prediction frees its place in your 5 predictions in progress at once.
Errors
Errors are JSON problem details, plus a stable code for programs:
{
"title": "Too many predictions in progress",
"detail": "This account can have up to 5 predictions queued or running at once, shared by its API keys and MCP connections, and 5 are. Wait for one to finish and try again.",
"status": 429,
"code": "concurrency_limit",
"retry_after": 10,
"limit": 5,
"running": 5
}invalid_fields. A 422 lists every field at fault:
{
"title": "Input validation failed",
"status": 422,
"code": "invalid_input",
"detail": "input.duration: At 720p the longest clip is 10 seconds.",
"invalid_fields": [
{ "type": "invalid_value", "field": "input.duration", "description": "At 720p the longest clip is 10 seconds." }
]
}