Quickstart
From zero to a downloaded file in five steps. You need an API key with the generate and read scopes, and some coins.
1. Pick a model
GET /v1/models is public. Each model has an id, a task type and its params with the values they accept. Send only the params you want to set: the others take the model default. See Models.
2. Quote
POST /v1/quotes returns the exact coins of the request and a quoteToken valid for 10 minutes.
3. Submit
POST /v1/generations with the same modelId and params, plus exactly one of quoteToken (charge exactly the quoted coins) or maxCoins (charge the current price, never more than this). The Idempotency-Key header is required: if a request fails on the network, retry it with the same key and you get the same generation instead of a second charge.
To skip the quote, submit with maxCoins. If the price is above it, the request fails with max_coins_exceeded and nothing is charged.
{
"modelId": "MODEL_ID",
"params": { "prompt": "A lighthouse at dusk, watercolor" },
"maxCoins": 40
}4. Wait for the result
Poll GET /v1/generations/:id every few seconds (no faster than every 2 seconds) until the status is succeeded, failed or canceled. Better still, register a webhook and get notified.
5. Download
Each item of outputs has a url. Links are signed and short-lived: download soon, or read the generation again (or GET /v1/assets/:id) for fresh links.
Full example
# 1. List the models (public, no key needed)
curl https://api.inker.si/v1/models
# 2. Quote: the exact coins this request costs, and a quoteToken valid 10 minutes
curl https://api.inker.si/v1/quotes \
-H "Authorization: Bearer $INKER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"modelId": "MODEL_ID", "params": {"prompt": "A lighthouse at dusk, watercolor"}}'
# 3. Submit with the quoteToken (or "maxCoins": N instead).
# Reuse the same Idempotency-Key when you retry the same request.
curl https://api.inker.si/v1/generations \
-H "Authorization: Bearer $INKER_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"modelId": "MODEL_ID", "params": {"prompt": "A lighthouse at dusk, watercolor"}, "quoteToken": "QUOTE_TOKEN"}'
# 4. Poll until status is succeeded, failed or canceled (or use webhooks)
curl https://api.inker.si/v1/generations/GENERATION_ID \
-H "Authorization: Bearer $INKER_API_KEY"
# 5. Download an output: outputs[].url is a short-lived signed link
curl -o output.png "OUTPUT_URL"// Node 18+ (global fetch). Keep the key on your server, never in a browser.
import { writeFile } from 'node:fs/promises';
const API = 'https://api.inker.si';
const KEY = process.env.INKER_API_KEY;
async function inker(path, { headers, ...init } = {}) {
const response = await fetch(`${API}${path}`, {
...init,
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json', ...headers },
});
const body = await response.json();
// Errors are RFC 9457 problem details with a stable `code`.
if (!response.ok) throw new Error(`${body.code}: ${body.detail}`);
return body;
}
// 1. Pick a model
const { models } = await inker('/v1/models');
const model = models.find((m) => m.taskType === 'image.generate');
const params = { prompt: 'A lighthouse at dusk, watercolor' };
// 2. Quote
const quote = await inker('/v1/quotes', {
method: 'POST',
body: JSON.stringify({ modelId: model.id, params }),
});
console.log(`This costs ${quote.coins} coins`);
// 3. Submit (one Idempotency-Key per intended generation; reuse it on retries)
let generation = await inker('/v1/generations', {
method: 'POST',
headers: { 'Idempotency-Key': crypto.randomUUID() },
body: JSON.stringify({ modelId: model.id, params, quoteToken: quote.quoteToken }),
});
// 4. Poll (or receive a webhook)
while (generation.status === 'queued' || generation.status === 'processing') {
await new Promise((resolve) => setTimeout(resolve, 3000));
generation = await inker(`/v1/generations/${generation.id}`);
}
// 5. Download the outputs
if (generation.status === 'succeeded') {
for (const [i, output] of generation.outputs.entries()) {
const file = await fetch(output.url);
await writeFile(`output-${i}`, Buffer.from(await file.arrayBuffer()));
}
}import os, time, uuid
import requests
API = "https://api.inker.si"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['INKER_API_KEY']}"
def inker(method, path, **kwargs):
response = session.request(method, API + path, **kwargs)
body = response.json()
if not response.ok:
# Errors are RFC 9457 problem details with a stable "code".
raise RuntimeError(f"{body['code']}: {body['detail']}")
return body
# 1. Pick a model
models = inker("GET", "/v1/models")["models"]
model = next(m for m in models if m["taskType"] == "image.generate")
params = {"prompt": "A lighthouse at dusk, watercolor"}
# 2. Quote
quote = inker("POST", "/v1/quotes", json={"modelId": model["id"], "params": params})
print(f"This costs {quote['coins']} coins")
# 3. Submit (one Idempotency-Key per intended generation; reuse it on retries)
generation = inker(
"POST",
"/v1/generations",
headers={"Idempotency-Key": str(uuid.uuid4())},
json={"modelId": model["id"], "params": params, "quoteToken": quote["quoteToken"]},
)
# 4. Poll (or receive a webhook)
while generation["status"] in ("queued", "processing"):
time.sleep(3)
generation = inker("GET", f"/v1/generations/{generation['id']}")
# 5. Download the outputs
if generation["status"] == "succeeded":
for i, output in enumerate(generation["outputs"]):
with open(f"output-{i}", "wb") as f:
f.write(requests.get(output["url"]).content)Using your own files
Models that take an image, audio or video need an upload first: send the file to POST /v1/uploads as multipart/form-data in the field file, then pass the returned id in the media param (for example image or images). Uploads are deleted after 30 days.
# Upload an input file (multipart field "file"), then pass its id in a media param
curl https://api.inker.si/v1/uploads \
-H "Authorization: Bearer $INKER_API_KEY" \
-F "file=@photo.png"import { readFile } from 'node:fs/promises';
const form = new FormData();
form.append('file', new Blob([await readFile('photo.png')], { type: 'image/png' }), 'photo.png');
const upload = await fetch('https://api.inker.si/v1/uploads', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.INKER_API_KEY}` },
body: form,
});
const asset = await upload.json(); // use asset.id in a media param, e.g. params.imagewith open("photo.png", "rb") as f:
asset = inker("POST", "/v1/uploads", files={"file": ("photo.png", f, "image/png")})
# use asset["id"] in a media param, e.g. params["image"]