Loading your account

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.

JSON
{
  "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

curl
# 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"
JavaScript
// 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()));
  }
}
Python
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.

curl
# 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"
JavaScript
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.image
Python
with 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"]