y83.media API v2

External generation API. Asynchronous image generation with Flux Klein 4B and Krea 2 Turbo models.

Base URL: https://media.y83.ru/api/v2

OpenAPI spec: openapi.yaml


Table of Contents


Overview

API v2 is designed for external integrations. All generation is async — you submit a job, receive an ID, then either poll for results or register a webhook.

Two models available:

Model Type Parameters Steps
flux2-klein-4b Flux.2 4B (GGUF Q4) 1–8 (default 4)
krea2-turbo Krea 2 12B distilled 1–9 (default 8)

krea2-turbo also does inpainting and outpainting — pass mask, mask_region or anypaint_padding, see Inpaint & Outpaint.


Authentication

All endpoints except /health, /models and /auth/login require a Bearer token.

Getting a token

Option 1 — login endpoint (recommended for integrations):

curl -X POST https://media.y83.ru/api/v2/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "secret", "name": "my-integration"}'

Response:

{
  "data": {
    "token": "1|abc123def456ghi789...",
    "token_type": "Bearer",
    "name": "my-integration",
    "created_at": "2026-08-28T12:00:00.000Z"
  },
  "meta": { "api_version": "2.0" }
}

Rate limited: 5 attempts/minute per email+IP, 20/minute per IP.

Option 2 — admin panel: an administrator can create and revoke tokens in the Filament admin (Users → API-токены).

Store the returned plain-text token — it is shown only once. Additional tokens can be created later with POST /api/v2/tokens using an existing token.

Using the token

Authorization: Bearer {your_api_token}

Rate Limiting

100 requests per minute per token.

HTTP headers included in every response:

Header Description
X-RateLimit-Limit Max requests per window (100)
X-RateLimit-Remaining Remaining requests in current window
X-RateLimit-Reset Unix timestamp when window resets

When exceeded, returns 429 Too Many Requests:

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Try again later."
  }
}

Endpoints

Login

POST /api/v2/auth/login

Public (rate limited). Exchange email/password for a personal access token.

Request body:

Field Type Required Description
email string yes Account email
password string yes Account password
name string no Token name (default api-v2)

Response (200): see Authentication.

Error (422): wrong credentials — error.details.email.


Health Check

GET /api/v2/health

Public. No auth required.

Response (200):

{
  "status": "ok",
  "api_version": "2.0",
  "timestamp": "2026-08-28T12:00:00.000Z"
}
curl https://media.y83.ru/api/v2/health

List Models

GET /api/v2/models

Public. Returns all models with defaults, limits, and features.

Response (200): See Models section for full structure.

curl https://media.y83.ru/api/v2/models

Create Generation

POST /api/v2/generate

Auth required. Submit an async generation job.

Retry-safe submissions: send an Idempotency-Key header (any stable string, ≤255 chars). Repeating the call with the same key and the same body returns the stored response of the first call with Idempotency-Replayed: true instead of queueing a second GPU job; the same key with a different body answers 409 CONFLICT. Keys live for 24 hours. Use it whenever a timeout left you unsure whether the request was received.

Request body:

Field Type Required Default Description
model string yes — flux2-klein-4b or krea2-turbo
prompt string yes — Text prompt (max 10,000 chars)
negative_prompt string no null What to avoid (max 5,000 chars)
seed integer no random Random seed
width integer no 1024 256–2048
height integer no 1024 256–2048
batch_size integer no 1 1–4 images
steps integer no model default Inference steps (flux2 1–8, krea2 1–9)
cfg number no 1.0 Classifier-free guidance (0–30)
image string no — Base64 data URI for img2img (PNG/JPEG/GIF/WebP, ≤2048px). For flux2 switches mode to img2img
image_url string no — Public http(s) URL of the source image (alternative to image, ≤8 MB). Private/loopback/reserved addresses rejected with 422
webhook_url string no — Callback URL (public http/https only; private/loopback addresses rejected with 422)
webhook_secret string no — HMAC secret

Krea 2 specific fields:

Field Type Default Description
megapixels number 1.0 Total megapixels (0.5–2.0)
krea_lora string — Primary LoRA ID
krea_lora_strength number 1.0 Primary LoRA strength (0–1.5)
krea_lora_2 string — Secondary LoRA ID
krea_lora_2_strength number 1.0 Secondary LoRA strength (0–1.5)
krea_4step boolean false 4-step distilled mode
style_refs array [] Up to 12 style references
concept_sliders array [] Up to 8 concept sliders
post_upscale string — 2x or 4x

Inpaint / outpaint fields (krea2 only, see Inpaint & Outpaint):

Field Type Description
mask string Base64 mask image, white = repaint
mask_region array Up to 8 {x, y, w, h} rectangles; the server draws the mask
anypaint_padding object {left, top, right, bottom} pixels to add around the frame
denoise number Repaint strength (0–1); omitted → 0.85 inpaint / 1.0 outpaint / 0.45 krea2 img2img. Use 1.0 for a real object replacement

Response (201):

{
  "data": {
    "id": "42",
    "status": "pending",
    "model": "flux2-klein-4b",
    "mode": "text2img",
    "operation": "text2img",
    "created_at": "2026-08-28T12:00:00.000Z"
  },
  "meta": { "api_version": "2.0" }
}

operation says what the server actually did — text2img, img2img, inpaint, outpaint or inpaint_outpaint. Clients never have to infer it from the request.

# Basic Flux generation
curl -X POST https://media.y83.ru/api/v2/generate \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "flux2-klein-4b",
    "prompt": "a beautiful sunset over mountains",
    "width": 1024,
    "height": 1024
  }'

# Krea 2 with LoRA and upscale
curl -X POST https://media.y83.ru/api/v2/generate \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "krea2-turbo",
    "prompt": "a cozy room with warm lighting, rainy window style",
    "krea_lora": "krea2_rainywindow",
    "krea_lora_strength": 0.8,
    "post_upscale": "2x"
  }'

# With webhook
curl -X POST https://media.y83.ru/api/v2/generate \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "krea2-turbo",
    "prompt": "cyberpunk city at night",
    "webhook_url": "https://myapp.com/webhooks/generation",
    "webhook_secret": "my_secret_key"
  }'

Inpaint & Outpaint

Available for krea2-turbo only. Nothing extra to switch on: passing a mask or padding is enough, the server picks the AnyPaint workflow and reports it back in operation.

Request contains operation Result
mask or mask_region inpaint Only the masked area is repainted
anypaint_padding (non-zero) outpaint Frame is extended, original stays intact
both inpaint_outpaint Extended frame + repaint inside the mask
image/image_url only img2img Whole image reworked, no mask
neither text2img Generation from scratch

Two ways to say what to repaint:

  1. mask_region — rectangles in pixels. The server renders the mask itself, so a client needs no image libraries at all:
curl -X POST https://media.y83.ru/api/v2/generate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "krea2-turbo",
    "prompt": "leather collar, studio light",
    "image_url": "https://media.y83.ru/uploads/gen_abc123.png",
    "mask_region": [
      { "x": 100, "y": 200, "w": 300, "h": 120 },
      { "x": 700, "y": 40, "w": 120, "h": 120 }
    ],
    "denoise": 1.0
  }'

Coordinates are in the source image's own pixels, not the displayed size. A rectangle that leaves the image bounds is rejected with 422 before anything is generated.

  1. mask — your own PNG. Must have the same pixel dimensions as the source image. White = repaint, black = keep.
curl -X POST https://media.y83.ru/api/v2/generate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "krea2-turbo",
    "prompt": "remove the scratch, keep everything else",
    "image": "data:image/png;base64,...",
    "mask": "data:image/png;base64,..."
  }'

Outpaint — extend the frame:

curl -X POST https://media.y83.ru/api/v2/generate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "krea2-turbo",
    "prompt": "continue the scene, snowy field",
    "image_url": "https://media.y83.ru/uploads/gen_abc123.png",
    "anypaint_padding": { "left": 0, "top": 128, "right": 256, "bottom": 0 }
  }'

Command-line helper for agents and scripts — builds the mask from rectangles, submits, polls and saves the result:

export Y83_API_TOKEN=1|abc123...

php scripts/krea-inpaint.php \
  --image=/tmp/cat.png \
  --prompt="leather collar" \
  --region=100,200,300,120 \
  --region=700,40,120,120 \
  --out=/tmp/result

# prints: /tmp/result/inpaint_42.png

--dry-run prints the request payload without sending it.

Gotchas


Get Generation Status

GET /api/v2/generate/{id}

Auth required. Poll for the result — or wait for it in one call.

Query parameters:

Field Type Default Description
wait integer 0 Long-poll: hold the request while the job runs and answer the moment it becomes terminal (status re-checked every 0.5 s). Allowed range 0–30. meta.waited_ms reports the actual wait.

A generation takes 20–80 s. Instead of a poll-and-sleep loop, call once with ?wait=30: the request returns as soon as the job is completed/failed, or with the current status when the 30 s limit expires — then just call it again. One waiting request occupies one PHP-FPM worker (the host runs pm.max_children = 5), so do not keep many waits in flight. If you prefer short calls, the response carries poll_after_ms — the suggested delay before the next poll.

Every parameter the job was submitted with is echoed back, so this endpoint is enough to repeat or continue a job after a restart:

Field Description
status pending, processing, retry_pending, completed, failed, dead_letter
mode, operation operation is text2img, img2img, inpaint, outpaint or inpaint_outpaint — what actually happened
progress, progress_text, queue_position Live progress; queue_position is 1-based and drops to 0 once the job left the queue
params prompt, negative_prompt, seed, steps, cfg, width, height, batch_size, denoise, megapixels, krea_lora(_strength), krea_lora_2(_strength), krea_4step, post_upscale, anypaint_padding
style_refs [{recipe, strength, direction, timing}] — recipes without the reference images
concept_sliders [{name, value, negative_pole, positive_pole}]
source {image_url, mask_url} — feed image_url straight into the next request
poll_after_ms Suggested delay before the next poll (0 for finished jobs)

Response (200):

Pending:

{
  "data": {
    "id": "42",
    "status": "pending",
    "model": "krea2-turbo",
    "mode": "krea2",
    "operation": "text2img",
    "progress": 0,
    "progress_text": "В очереди",
    "queue_position": 1,
    "poll_after_ms": 2000,
    "created_at": "2026-08-28T12:00:00.000Z",
    "started_at": null,
    "completed_at": null,
    "params": {
      "prompt": "a wooden mug on a kitchen table",
      "seed": 1234567890,
      "steps": 8,
      "cfg": 1.0,
      "width": 1024,
      "height": 1024,
      "batch_size": 1,
      "megapixels": 1.0
    },
    "style_refs": [],
    "concept_sliders": [],
    "source": null
  },
  "meta": { "api_version": "2.0", "waited_ms": 0 }
}

Completed:

{
  "data": {
    "id": "42",
    "status": "completed",
    "model": "flux2-klein-4b",
    "progress": 100,
    "created_at": "2026-08-28T12:00:00.000Z",
    "completed_at": "2026-08-28T12:00:15.000Z",
    "result": {
      "images": [
        {
          "url": "https://media.y83.ru/uploads/gen_abc123.png",
          "width": 1024,
          "height": 1024
        }
      ],
      "seed": 1234567890,
      "model": "flux2-klein-4b",
      "generation_time_ms": 14523,
      "width": 1024,
      "height": 1024
    }
  },
  "meta": { "api_version": "2.0" }
}

Failed:

{
  "data": {
    "id": "42",
    "status": "failed",
    "model": "krea2-turbo",
    "progress": 0,
    "created_at": "2026-08-28T12:00:00.000Z",
    "completed_at": null,
    "error": {
      "code": "GENERATION_FAILED",
      "message": "Model inference timed out"
    }
  },
  "meta": { "api_version": "2.0" }
}
curl https://media.y83.ru/api/v2/generate/42 \
  -H "Authorization: Bearer {api_token}"

# One call instead of a poll loop: waits up to 30 s for the result
curl "https://media.y83.ru/api/v2/generate/42?wait=30" \
  -H "Authorization: Bearer {api_token}"

List Own Generations

GET /api/v2/generations

Auth required. The caller's own generations, newest first. Unlike the v1 gallery, unfinished jobs are included — an agent that lost a job ID can see the job is still running instead of assuming it vanished and submitting a duplicate.

Query parameters:

Field Type Default Description
page integer 1 Page number
per_page integer 20 Items per page (1–100)
status string — pending, processing, completed, failed, retry_pending, dead_letter
mode string — text2img, img2img, krea2
model string — Model ID
include string — params adds the parameter echo, progress, operation, source, style_refs and concept_sliders to every item

Response (200):

{
  "data": [
    {
      "id": "1233",
      "status": "completed",
      "model": "krea2-turbo",
      "mode": "krea2",
      "prompt": "a wooden mug on a kitchen table",
      "seed": 2560234670541161070,
      "created_at": "2026-08-28T12:00:00.000Z",
      "result": {
        "images": [
          { "url": "https://media.y83.ru/uploads/gen_abc123.png", "width": 1024, "height": 1024 }
        ],
        "seed": 2560234670541161070,
        "generation_time_ms": 8432
      }
    }
  ],
  "meta": { "api_version": "2.0", "page": 1, "per_page": 20, "total": 1, "has_more": false }
}

result is present only for completed jobs. The width/height there are exactly what you need to compute the coordinates of the next mask_region — and the url is what you feed into image_url. That closes the loop for multi-step edits.

With include=params each item also carries the full parameter echo — handy to rebuild your context after a restart without a status call per job:

curl -s "https://media.y83.ru/api/v2/generations?include=params&per_page=5" \
  -H "Authorization: Bearer {api_token}"
# Continue an edit chain: take the last result and repaint a corner of it
LAST=$(curl -s "https://media.y83.ru/api/v2/generations?status=completed&per_page=1" \
  -H "Authorization: Bearer $TOKEN" | jq -r '.data[0].result.images[0].url')

curl -X POST https://media.y83.ru/api/v2/generate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg url "$LAST" '{
    model: "krea2-turbo",
    prompt: "add a second plant on the windowsill",
    image_url: $url,
    mask_region: [{x: 700, y: 40, w: 160, h: 160}],
    denoise: 1.0
  }')"

Get Quota

GET /api/v2/quota

Auth required. How much of the 100 requests/minute budget is already spent — the same numbers as the x-ratelimit-limit / x-ratelimit-remaining headers sent with every response, so a client can wait before the next call instead of walking into a 429. The counter includes the /quota request itself.

Response (200):

{
  "data": { "limit": 100, "used": 5, "remaining": 95, "resets_in_seconds": 42 },
  "meta": { "api_version": "2.0" }
}
curl https://media.y83.ru/api/v2/quota -H "Authorization: Bearer {api_token}"

Cancel Generation

DELETE /api/v2/generate/{id}

Auth required. Cancel a pending or processing generation.

Response (200):

{
  "data": {
    "id": "42",
    "status": "failed"
  },
  "meta": { "api_version": "2.0" }
}

Error (409): Generation already completed or failed.

{
  "error": {
    "code": "INVALID_STATE",
    "message": "Generation cannot be cancelled in current status: completed"
  },
  "meta": { "api_version": "2.0" }
}
curl -X DELETE https://media.y83.ru/api/v2/generate/42 \
  -H "Authorization: Bearer {api_token}"

List Tokens

GET /api/v2/tokens

Auth required. List all personal access tokens.

Response (200):

{
  "data": [
    {
      "id": 1,
      "name": "my-integration",
      "last_used_at": "2026-08-27T10:30:00.000Z",
      "created_at": "2026-08-20T08:00:00.000Z"
    }
  ],
  "meta": { "api_version": "2.0" }
}
curl https://media.y83.ru/api/v2/tokens \
  -H "Authorization: Bearer {api_token}"

Create Token

POST /api/v2/tokens

Auth required (Bearer token). Create an additional personal access token.

Request body:

Field Type Required Description
name string yes Token name (max 255 chars)

Response (201):

{
  "data": {
    "id": 3,
    "name": "my-integration",
    "token": "1|abc123def456ghi789...",
    "created_at": "2026-08-28T12:00:00.000Z"
  },
  "meta": { "api_version": "2.0" }
}

Warning: The plain-text token is only shown in this response. Store it securely.

curl -X POST https://media.y83.ru/api/v2/tokens \
  -H "Authorization: Bearer {api_token}" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-integration"}'

Revoke Token

DELETE /api/v2/tokens/{id}

Auth required. Permanently revoke a token.

Response (200):

{
  "data": {
    "message": "Token revoked"
  },
  "meta": { "api_version": "2.0" }
}
curl -X DELETE https://media.y83.ru/api/v2/tokens/3 \
  -H "Authorization: Bearer {api_token}"

Models

Flux Klein 4B

Fast Flux.2 model. 4B parameters, GGUF quantized. Optimized for speed.

Parameter Default Range
steps 4 1–8
cfg 1.0 0–30
width 1024 256–2048
height 1024 256–2048

Features: text2img, img2img (pass image as base64 data URI)

Krea 2 Turbo

Krea 2 Turbo diffusion transformer. 12B params distilled to 8 steps. Supports LoRAs, style references, and concept sliders.

Parameter Default Range
steps 8 1–9
cfg 1.0 0–30
width 1024 256–2048
height 1024 256–2048
megapixels 1.0 0.5–2.0

Features: text2img, img2img, inpaint, outpaint, style_references, concept_sliders, loras, post_upscale, 4step

Available LoRAs

ID Name Trigger Phrase
krea2_darkbrush Dark Brush monochrome ink wash style
krea2_dotmatrix Dot Matrix Monochrome stippling style
krea2_kidsdrawing Kids Drawing naive expressive sketch style
krea2_neondrip Neon Drip Textured abstract style
krea2_rainywindow Rainy Window rainy window style
krea2_retroanime Retro Anime Purple retro anime style
krea2_softwatercolor Soft Watercolor Art Deco watercolor style
krea2_sunsetblur Sunset Blur ethereal motion blur style
krea2_vintagetarot Vintage Tarot vintage tarot style
krea2_enhancer Enhancer universal quality boost
krea2_realism_v2 Realism V2 photorealism
krea2_ideokrea IdeoKrea Ideogram 4 style

Style Reference Recipes

The recipe field in style_refs controls how the reference image influences generation:

Recipe Description
balanced Balanced mix of all style aspects
keep the same subject Preserve subject, restyle everything
suggest the visual style Apply visual style to new subject
copy lighting and mood Match lighting and atmosphere
suggest material or texture Apply surface/material properties
avoid copying text/logos Style without reproducing text
suggest the color palette Match color scheme
use the background/setting Apply environment/background
copy the camera framing Match composition and framing

The full recipe list (starter / designer / edit packs — 31 values) is defined in openapi.yaml (StyleRef.recipe enum). Unknown values are rejected with 422.

Concept Sliders

Fine-grained control via named sliders:

{
  "concept_sliders": [
    {
      "name": "blur",
      "value": -3.5,
      "negative_pole": "sharp focused",
      "positive_pole": "blurry dreamy"
    }
  ]
}

Value range: -6 (negative pole) to +6 (positive pole). Max 8 sliders.


Webhooks

When you provide a webhook_url, we POST a payload to that URL when the generation completes or fails.

Note: webhook_url must be a public http/https address. Private, loopback and reserved ranges (localhost, 127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16, link-local, CGNAT, etc.) are rejected at validation and re-checked right before delivery (SSRF protection).

Payload

{
  "event": "generation.completed",
  "data": {
    "id": "42",
    "status": "completed",
    "model": "krea2-turbo",
    "result": {
      "images": [
        { "url": "https://media.y83.ru/uploads/gen_abc123.png", "width": 1024, "height": 1024 }
      ],
      "seed": 1234567890,
      "generation_time_ms": 8432
    }
  },
  "meta": { "api_version": "2.0" }
}

Events: generation.completed, generation.failed

HMAC Verification

Each webhook request includes a signature header for verification:

X-Webhook-Signature: sha256={hex_digest}

Verification (Python)

import hmac
import hashlib

def verify_webhook(payload_body: bytes, signature_header: str, secret: str) -> bool:
    """Verify webhook HMAC-SHA256 signature."""
    expected = 'sha256=' + hmac.new(
        secret.encode(),
        payload_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

Verification (Node.js)

const crypto = require('crypto');

function verifyWebhook(payloadBody, signatureHeader, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payloadBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}

Verification (PHP)

function verifyWebhook(string $payloadBody, string $signatureHeader, string $secret): bool
{
    $expected = 'sha256=' . hash_hmac('sha256', $payloadBody, $secret);
    return hash_equals($expected, $signatureHeader);
}

Example handler (Express.js)

app.post('/webhooks/generation', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  if (!verifyWebhook(req.body, signature, 'my_secret_key')) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(req.body);
  console.log(`Generation ${event.data.id}: ${event.data.status}`);

  // Process result...
  res.status(200).json({ received: true });
});

Error Codes

Code HTTP Status Description
VALIDATION_ERROR 422 Request body failed validation
UNAUTHENTICATED 401 Missing or invalid Bearer token
FORBIDDEN 403 Token lacks required permissions
NOT_FOUND 404 Resource does not exist or belongs to another user
INVALID_STATE 409 Operation not allowed in current state
RATE_LIMIT_EXCEEDED 429 Too many requests (100/min per token; login 5/min per email+IP)
GENERATION_FAILED — Generation failed (see error message)
cancelled — Generation was cancelled by user

Validation error format

{
  "error": {
    "code": "VALIDATION_ERROR",
    "http_status": 422,
    "message": "Ошибка валидации",
    "details": {
      "model": ["Model is required"],
      "steps": ["The steps field must not be greater than 8."]
    }
  },
  "meta": { "api_version": "2.0" }
}

Nested fields keep their dotted key, so the index of the offending rectangle is visible:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "http_status": 422,
    "message": "Ошибка валидации",
    "details": {
      "mask_region.0.w": ["mask_region[].w must be an integer from 1 to 2048 (width, source pixels)"],
      "anypaint_padding.left": ["anypaint_padding.left must be an integer from 0 to 2048"]
    }
  },
  "meta": { "api_version": "2.0" }
}

Note for clients parsing details as a JSON path: keys like mask_region.0.w contain dots, so read them as plain map keys rather than walking the tree.


Examples

Agent quickstart

Everything an autonomous agent needs, in order. No session, no CSRF — just a Bearer token.

BASE=https://media.y83.ru/api/v2

# 1. Discover capabilities (public). Includes links to these docs.
curl -s $BASE/health

# 2. Authenticate once, reuse the token
TOKEN=$(curl -s -X POST $BASE/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "agent@example.com", "password": "...", "name": "agent"}' \
  | jq -r '.data.token')

# 3. Check what krea2-turbo can do: inpaint contract, recipes, sliders
curl -s $BASE/models | jq '.data["krea2-turbo"] | {features, inpaint, recipes: .recipes[0:3], concept_sliders}'

# 4. Generate a base image. Idempotency-Key makes the call safe to retry:
#    a repeat with the same key returns the same job instead of a second one.
JOB=$(curl -s -X POST $BASE/generate \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: quickstart-mug" \
  -d '{"model":"krea2-turbo","prompt":"a wooden mug on a kitchen table"}' \
  | jq -r '.data.id')

# 5. Wait for it in one call. The job takes ~20-80 s, so repeat the call while
#    the status is not terminal. poll_after_ms tells a sane interval if you
#    prefer short calls.
RESULT=$(curl -s "$BASE/generate/$JOB?wait=30" -H "Authorization: Bearer $TOKEN")
while echo "$RESULT" | jq -e '.data.status | test("pending|processing|retry_pending")' > /dev/null; do
  RESULT=$(curl -s "$BASE/generate/$JOB?wait=30" -H "Authorization: Bearer $TOKEN")
done

URL=$(echo "$RESULT" | jq -r '.data.result.images[0].url')

# 6. Repaint a region of it — coordinates in the image's own pixels
curl -s -X POST $BASE/generate \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "$(jq -n --arg url "$URL" '{
    model: "krea2-turbo",
    prompt: "replace the wooden mug with a ceramic teapot",
    image_url: $url,
    mask_region: [{x: 200, y: 300, w: 600, h: 400}],
    denoise: 1.0
  }')"

# 7. If you lost a job ID, find it here
curl -s "$BASE/generations?per_page=5" -H "Authorization: Bearer $TOKEN"

Or run steps 4–6 in one shot with the bundled helper:

export Y83_API_TOKEN="$TOKEN"
php scripts/krea-inpaint.php \
  --image="$URL" \
  --prompt="replace the wooden mug with a ceramic teapot" \
  --region=200,300,600,400 \
  --denoise=1 \
  --out=./result

Complete workflow: Flux Klein

# 1. Get a token
TOKEN=$(curl -s -X POST https://media.y83.ru/api/v2/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "secret", "name": "cli-tool"}' | jq -r '.data.token')

# 2. Submit generation
RESPONSE=$(curl -s -X POST https://media.y83.ru/api/v2/generate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "flux2-klein-4b",
    "prompt": "a serene Japanese garden with koi pond",
    "width": 1024,
    "height": 768,
    "steps": 6
  }')

JOB_ID=$(echo $RESPONSE | jq -r '.data.id')
echo "Job ID: $JOB_ID"

# 3. Poll until completed
while true; do
  STATUS=$(curl -s https://media.y83.ru/api/v2/generate/$JOB_ID \
    -H "Authorization: Bearer $TOKEN")
  STATE=$(echo $STATUS | jq -r '.data.status')
  echo "Status: $STATE"

  if [ "$STATE" = "completed" ]; then
    echo $STATUS | jq '.data.result.images[].url'
    break
  elif [ "$STATE" = "failed" ]; then
    echo $STATUS | jq '.data.error'
    break
  fi

  sleep 3
done

Complete workflow: Krea 2 with LoRA

# Submit Krea 2 generation with Rainy Window LoRA + upscale
curl -X POST https://media.y83.ru/api/v2/generate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "krea2-turbo",
    "prompt": "a cat sitting on a windowsill, rainy city outside",
    "krea_lora": "krea2_rainywindow",
    "krea_lora_strength": 0.85,
    "post_upscale": "2x",
    "megapixels": 1.5,
    "width": 1024,
    "height": 768
  }'

Webhook integration

# Submit with webhook — no polling needed
curl -X POST https://media.y83.ru/api/v2/generate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "krea2-turbo",
    "prompt": "abstract digital art, vibrant colors",
    "webhook_url": "https://myapp.com/api/webhooks/y83",
    "webhook_secret": "whsec_mysupersecretkey"
  }'

Error handling

# Cancel a generation
curl -X DELETE https://media.y83.ru/api/v2/generate/42 \
  -H "Authorization: Bearer $TOKEN"

# If already completed, returns 409:
# {
#   "error": {
#     "code": "INVALID_STATE",
#     "message": "Generation cannot be cancelled in current status: completed"
#   }
# }