API Reference · v0.4

Runfra API

Async batch image generation. Submit a prompt, Runfra generates multiple candidates in parallel, scores and filters them, and returns the best result. No SDK required — plain HTTP.

Base URL:https://runfra-production.up.railway.app
Overview

The Runfra API follows an async job model. Every image generation request creates a job that runs asynchronously across available GPU workers. You poll for results; no webhooks needed.

Lifecycle
1.
POST /v1/jobs

Submit a prompt (or items[]) — returns a job_id immediately

2.
GET /v1/jobs/{id}/result

Poll until HTTP 200 (job complete); 202 means still running

3.
best_result_url / items[]

Single-prompt: read best_result_url. Multi-prompt: read items[].result_url.

Authentication

Two auth modes are accepted. For production server-to-server use, always use an API key. Bearer tokens are short-lived and intended for quick testing only.

API Key — Production
X-API-Key: rfa_your_key_here
  • • Long-lived, revocable credential
  • • Format: rfa_ + 40 hex chars
  • • Raw value shown once at creation — save it
  • • Revoke without affecting other keys
  • • Create & manage at Dashboard → API Keys
Bearer Token — Testing only
Authorization: Bearer <supabase_access_token>
  • • Short-lived (~1 hour)
  • • Copy from Dashboard → API Quickstart
  • • Do not use in production code
  • • Do not commit to a repo
  • • Required for /v1/me/* routes
Key typeBilling
customerProduction key linked to your account. Credits deducted on job creation; refunded automatically on failure.
developerInternal/CI key. Billing bypassed. Not available via self-service — requires internal provisioning.

All keys created through the dashboard are customer type.

Models

Pass the model name (or any alias) in the model_name field when creating a job. If omitted, defaults to SDXL.

SDXL — Default
stable-diffusion-xl-base-1.0

Best for photorealistic images, illustrations, and general-purpose generation.

Aliases: sdxl · sdxl-base · stable-diffusion-xl
Default steps: 20
Guidance scale: configurable (default ~7.5)
ResolutionCredits / image
≤ 512 px1
≤ 768 px2
> 768 px (e.g. 1024×1024)4
FLUX Schnell — Fast
flux-schnell

High-quality, fast model. Good for abstract, artistic, and creative generation.

Aliases: flux · flux_schnell
Default steps: 4 (distilled — fewer steps intentional)
Guidance scale: forced to 0.0 (model design — request value ignored)
ResolutionCredits / image
≤ 512 px3
≤ 768 px6
> 768 px (e.g. 1024×1024)8
Cost = credits_per_image × effective_batch. Single mode: effective_batch = batch_size. Multi mode: effective_batch = items.length. Credits are frozen at job creation; refunds return the exact stored amount.
Parameters

All parameters are sent as JSON in the request body of POST /v1/jobs.

Two submission modes are available — set exactly one of prompt or items. They are mutually exclusive.

FieldTypeRequiredDefaultNotes
promptstringone ofSingle-prompt mode. 1–2000 characters. Mutually exclusive with items.
itemsarrayone ofMulti-prompt mode. Array of PromptItem — each produces one image. Mutually exclusive with prompt.
negative_promptstringnonullThings to avoid (single mode only). Max 2000 characters.
model_namestringnostable-diffusion-xl-base-1.0Model name or alias. See Models section.
widthintegerno1024Output width in pixels. Range: 512–1024.
heightintegerno1024Output height in pixels. Range: 512–1024.
num_inference_stepsintegernomodel defaultDenoising steps. Range: 1–100. Default: 20 (SDXL), 4 (FLUX). Lower = faster, lower quality.
guidance_scalefloatnomodel default0.0–20.0. How closely output follows the prompt. Ignored for flux-schnell (forced to 0.0).
batch_sizeintegerno1Single mode only. Candidate images to generate. 1–100. Larger batches improve Top Pick quality. Tier caps: anonymous = 4, free = 8, paid = 100.
quality_modestringno"strict"strict · soft · off. Controls quality filter aggressiveness.
return_all_candidatesbooleannofalseInclude rejected candidates in the result alongside accepted ones.
notify_on_completebooleannofalseSend a completion email when the job finishes. Requires a signed-in user with a verified email.

PromptItem fields (multi-prompt mode):

FieldTypeRequiredNotes
promptstringyes1–2000 characters. Prompt for this specific image.
negative_promptstringnoThings to avoid for this item.
seedintegerno0–4294967295. Seed for this item. Omit for a random seed.
Create Job
POST/v1/jobs
Accepts either auth mode. Credits deducted atomically; refunded automatically if the job fails.
Use prompt for a single scene, or items[] to generate multiple distinct prompts in one request.
bashSingle-prompt mode (API key)
curl -X POST https://runfra-production.up.railway.app/v1/jobs \
  -H "X-API-Key: rfa_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "a red panda on a wooden bridge, studio ghibli style",
    "model_name": "stable-diffusion-xl-base-1.0",
    "batch_size": 4,
    "quality_mode": "strict"
  }'
bashMulti-prompt mode — one image per prompt
curl -X POST https://runfra-production.up.railway.app/v1/jobs \
  -H "X-API-Key: rfa_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "prompt": "a fox in a snowy forest, golden hour" },
      { "prompt": "a whale diving underwater, photorealistic" },
      { "prompt": "a green parrot on a branch, oil painting", "seed": 42 }
    ],
    "model_name": "stable-diffusion-xl-base-1.0",
    "notify_on_complete": true
  }'
jsonResponse — 201 Created
{
  "job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "queued"
}
CodeReason
201Job created. Body contains job_id and status.
401Missing or invalid API key / JWT.
402Insufficient credits — balance < effective_batch × credits_per_image.
422Invalid field (both prompt and items set, neither set, empty items, empty item prompt, out-of-range value, batch limit exceeded). detail explains which constraint failed.
500Failed to enqueue tasks.
Async Polling

Jobs are asynchronous. Poll GET /v1/jobs/{job_id}/result until you receive 200 OK. While processing, the endpoint returns 202 Accepted.

Status/result returnsMeaning
queued202Waiting for a worker — keep polling.
running202Worker is generating candidates — keep polling.
succeeded200All tasks done; result available.
failed200Platform error; credits refunded automatically.
cancelled200Job cancelled — terminal, will never proceed. Returns 200 (not 202) so clients stop polling.
WhenField to use
Job is runningpreview_best_url — rolling best-so-far, non-null while running.
Job succeeded and selection_finalized = truebest_result_url — authoritative Top Pick. Use this in production.
After finalization, style preferenceaesthetic_best_url — highest aesthetic-score candidate.
All accepted candidatesresult_urls — array of all quality-passed image URLs.
Always check selection_finalized = true before reading best_result_url. It is null until finalization is complete.

Result routing: single-prompt clients read best_result_url; multi-prompt clients should read items[].result_url — one URL per submitted prompt. best_result_url is null for multi-mode jobs.

bashPolling loop
JOB_ID="a1b2c3d4-e5f6-7890-abcd-ef1234567890"

while true; do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    -H "X-API-Key: rfa_your_key_here" \
    "https://runfra-production.up.railway.app/v1/jobs/$JOB_ID/result")

  if [ "$STATUS" == "200" ]; then
    curl -s -H "X-API-Key: rfa_your_key_here" \
      "https://runfra-production.up.railway.app/v1/jobs/$JOB_ID/result"
    break
  fi

  echo "Still processing... ($STATUS)"
  sleep 3
done
Typical generation time: 10–30 seconds per task.
Endpoints
GET/v1/jobs/{job_id}/resultLightweight polling. Returns 200 when finalized.
GET/v1/jobs/{job_id}Full job object — includes selection_finalized, preview_best_url, task_progress.
Result Payload
GET/v1/jobs/{job_id}/resultLightweight polling. 200 when finalized; 202 while running.

The response shape is a superset for both single-prompt and multi-prompt jobs — no breaking changes for existing polling code.

jsonGET /v1/jobs/{job_id}/result — single-prompt, succeeded
{
  "job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "succeeded",
  "best_result_url": "https://storage.example.com/results/best.png",
  "result_urls": ["https://storage.example.com/results/0.png"],
  "accepted_count": 3,
  "quality_score": 0.87,
  "quality_passed": true,
  "execution_time_ms": 13200,
  "candidates": [{ "index": 0, "url": "...", "score": 0.87, "passed": true, "reasons": [] }],
  "input_mode": "single",
  "prompt_count": 1,
  "notify_on_complete": false,
  "items": null
}
jsonGET /v1/jobs/{job_id}/result — multi-prompt, succeeded
{
  "job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "succeeded",
  "best_result_url": null,
  "result_urls": [],
  "accepted_count": 2,
  "quality_score": 0.84,
  "quality_passed": true,
  "execution_time_ms": 28500,
  "candidates": [],
  "input_mode": "multi",
  "prompt_count": 3,
  "notify_on_complete": true,
  "items": [
    {
      "task_index": 0,
      "prompt": "a fox in a snowy forest",
      "status": "succeeded",
      "result_url": "https://storage.example.com/results/0.png",
      "seed": 42,
      "error_message": null
    },
    {
      "task_index": 1,
      "prompt": "a whale diving underwater",
      "status": "succeeded",
      "result_url": "https://storage.example.com/results/1.png",
      "seed": 7291,
      "error_message": null
    },
    {
      "task_index": 2,
      "prompt": "a green parrot on a branch",
      "status": "failed",
      "result_url": null,
      "seed": 9012,
      "error_message": "Quality gate: no passing candidates after 3 attempts"
    }
  ]
}
FieldNotes
input_modesingle (classic) or multi (items-based).
prompt_countNumber of distinct prompts submitted. Always 1 for single-mode.
notify_on_completeWhether a completion email was requested. One email per job — not per item.
itemsPer-item results. null for single-mode jobs. Authoritative source for multi-mode — use this over best_result_url when input_mode="multi".
best_result_urlAuthoritative Top Pick for single-mode. May be null for multi-mode — use items[].result_url instead.
result_urlsAll accepted candidate URLs (single-mode). Empty for multi-mode — use items[].result_url.
accepted_countNumber of quality-passed images. Authoritative.
quality_scoreBest quality score. null for in-progress or all-failed jobs.
quality_passedtrue if at least one image passed the quality gate.
execution_time_msWall-clock ms from started_at to finished_at.

items[] entry fields (multi-mode only):

FieldNotes
task_index0-based index matching submission order.
promptThe prompt for this specific item.
statusqueued · running · succeeded · failed
result_urlImage URL. null if the task failed or files have expired.
seedSeed used for this item's generation.
error_messageFailure reason. null for successful tasks.
GET/v1/jobs/{job_id}Full job object — includes selection_finalized, preview_best_url, task_progress.
jsonGET /v1/jobs/{job_id} — single-prompt, succeeded
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "succeeded",
  "prompt": "a photorealistic fox in a snowy forest, golden hour",
  "model_name": "stable-diffusion-xl-base-1.0",
  "created_at": "2024-01-15T10:30:00Z",
  "started_at": "2024-01-15T10:30:05Z",
  "finished_at": "2024-01-15T10:30:18Z",

  "selection_finalized": true,
  "best_result_url": "https://storage.example.com/results/best.png",
  "preview_best_url": null,
  "aesthetic_best_url": "https://storage.example.com/results/aesthetic.png",
  "result_urls": [
    "https://storage.example.com/results/0.png",
    "https://storage.example.com/results/1.png"
  ],

  "accepted_count": 3,
  "failed_count": 1,
  "total_attempts": 4,
  "progress": 1.0,
  "execution_time_ms": 13200,
  "quality_score": 0.87,
  "quality_passed": true,
  "is_best_effort": false,
  "error_message": null,
  "task_progress": null,

  "input_mode": "single",
  "prompt_count": 1,
  "notify_on_complete": false,
  "items": null
}
FieldNotes
selection_finalizedfalse while running. true once all tasks done and best result selected. Check this before using best_result_url.
best_result_urlAuthoritative Top Pick. Null until selection_finalized = true. Use this in production (single-mode).
preview_best_urlRolling best-so-far during generation. Non-null only while running; null after finalization.
aesthetic_best_urlHighest aesthetic-score candidate. Non-null only after finalization.
result_urlsAll accepted candidate image URLs (single-mode).
is_best_efforttrue when job succeeded but no images passed quality — images delivered below preferred threshold, no refund.
task_progressArray of per-task progress objects. Only present when status = running.
input_modesingle or multi.
itemsPer-item results. null for single-mode. Populated for multi-mode — authoritative source.

result_meta — legacy JSONB blob written by the worker. Always prefer top-level fields. Use result_meta only for fields with no top-level equivalent: candidates, user_selected_index, model_best_index, aesthetic_best_index.

Quality delivery tiers
quality_passedis_best_effortMeaning
truefalseNormal delivery — at least one image passed the quality filter.
falsetrueBest-effort delivery — images generated but none passed quality threshold. Best available image delivered. Credits not refunded.
falsefalsePlatform failure — no images generated at all. Credits refunded automatically.
Credits & Billing

Credits are deducted atomically when a job is created and refunded automatically if the job fails with no images delivered. Both Bearer JWT users and customer API key holders are subject to the same credit formula.

ScenarioCredits
Job createdDeducted immediately (effective_batch × credits_per_image)
Job succeeded (quality_passed = true)No refund — normal outcome
Job succeeded (is_best_effort = true)No refund — images delivered, below threshold
Job failed (is_best_effort = false)Full refund — no images generated
Content policy rejection at submissionNo charge — rejected before any work begins
Check balance
curl https://runfra-production.up.railway.app/v1/me/credits \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
  "credits": 487,
  "images_left": 487
}

Requires Bearer JWT. credits is your raw balance. images_left is an alias.

Credit packs
Starter — $5
500 credits · $0.01 / credit
Best Deal — $12
1,300 credits · ~$0.009 / credit

Purchase via Dashboard → Billing, or POST /v1/billing/checkout (Bearer JWT).

Errors

All error responses return a JSON object with a detail field explaining the reason.

CodedetailCause
401Invalid or inactive API keyAPI key revoked, expired, or malformed.
401Invalid or expired tokenBearer JWT expired or invalid.
402Not enough credits. Purchase more to continue.Credit balance below job cost.
403Access deniedJWT user or customer key accessing a job they don't own.
404Job not foundJob ID does not exist or belongs to another account.
409Cannot select a candidate while job is '...'Job not yet completed, or no accepted candidates.
422This request violates our content policy.Prompt blocked at submission. No credits charged.
422(field validation detail)Missing prompt, out-of-range value, batch_size exceeds tier cap.
429(Retry-After header set)Rate limit exceeded. See Rate Limits section.
500Failed to create jobPlatform error — safe to retry.
Rate Limits

All endpoints are rate-limited per identity. Exceeding the limit returns 429 Too Many Requests with a Retry-After header. Identity is resolved in this order: X-API-Key → Bearer JWT sub → client IP.

EndpointLimit
POST /v1/jobs20 req / min
GET /v1/jobs/{id}120 req / min
GET /v1/jobs/{id}/result120 req / min
POST /v1/jobs/{id}/select30 req / min
POST /v1/billing/checkout5 req / min
File Retention

Generated image files are stored for 30 days after job creation. Job metadata (prompt, status, accepted count, credits) is retained indefinitely.

storage_stateMeaningURLs returned?
activeFiles available in storage.Yes
pending_deleteScheduled for deletion in next cleanup run.Yes — download now
deletedFiles removed; only metadata remains.No — all URLs null
delete_failedStorage deletion failed; will retry.Yes
FieldNotes
files_expire_atISO timestamp — when files are scheduled for deletion.
storage_stateOne of: active, pending_delete, deleted, delete_failed.
files_deleted_atPopulated when cleanup marks the job deleted.
Runfra API Reference · v0.4