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.
https://runfra-production.up.railway.appThe 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.
POST /v1/jobsSubmit a prompt (or items[]) — returns a job_id immediately
GET /v1/jobs/{id}/resultPoll until HTTP 200 (job complete); 202 means still running
best_result_url / items[]Single-prompt: read best_result_url. Multi-prompt: read items[].result_url.
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.
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
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 type | Billing |
|---|---|
customer | Production key linked to your account. Credits deducted on job creation; refunded automatically on failure. |
developer | Internal/CI key. Billing bypassed. Not available via self-service — requires internal provisioning. |
All keys created through the dashboard are customer type.
Pass the model name (or any alias) in the model_name field when creating a job. If omitted, defaults to SDXL.
stable-diffusion-xl-base-1.0Best for photorealistic images, illustrations, and general-purpose generation.
sdxl · sdxl-base · stable-diffusion-xl20| Resolution | Credits / image |
|---|---|
| ≤ 512 px | 1 |
| ≤ 768 px | 2 |
| > 768 px (e.g. 1024×1024) | 4 |
flux-schnellHigh-quality, fast model. Good for abstract, artistic, and creative generation.
flux · flux_schnell4 (distilled — fewer steps intentional)| Resolution | Credits / image |
|---|---|
| ≤ 512 px | 3 |
| ≤ 768 px | 6 |
| > 768 px (e.g. 1024×1024) | 8 |
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.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.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
prompt | string | one of | — | Single-prompt mode. 1–2000 characters. Mutually exclusive with items. |
items | array | one of | — | Multi-prompt mode. Array of PromptItem — each produces one image. Mutually exclusive with prompt. |
negative_prompt | string | no | null | Things to avoid (single mode only). Max 2000 characters. |
model_name | string | no | stable-diffusion-xl-base-1.0 | Model name or alias. See Models section. |
width | integer | no | 1024 | Output width in pixels. Range: 512–1024. |
height | integer | no | 1024 | Output height in pixels. Range: 512–1024. |
num_inference_steps | integer | no | model default | Denoising steps. Range: 1–100. Default: 20 (SDXL), 4 (FLUX). Lower = faster, lower quality. |
guidance_scale | float | no | model default | 0.0–20.0. How closely output follows the prompt. Ignored for flux-schnell (forced to 0.0). |
batch_size | integer | no | 1 | Single mode only. Candidate images to generate. 1–100. Larger batches improve Top Pick quality. Tier caps: anonymous = 4, free = 8, paid = 100. |
quality_mode | string | no | "strict" | strict · soft · off. Controls quality filter aggressiveness. |
return_all_candidates | boolean | no | false | Include rejected candidates in the result alongside accepted ones. |
notify_on_complete | boolean | no | false | Send a completion email when the job finishes. Requires a signed-in user with a verified email. |
PromptItem fields (multi-prompt mode):
| Field | Type | Required | Notes |
|---|---|---|---|
prompt | string | yes | 1–2000 characters. Prompt for this specific image. |
negative_prompt | string | no | Things to avoid for this item. |
seed | integer | no | 0–4294967295. Seed for this item. Omit for a random seed. |
/v1/jobsprompt for a single scene, or items[] to generate multiple distinct prompts in one request.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"
}'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
}'{
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "queued"
}| Code | Reason |
|---|---|
| 201 | Job created. Body contains job_id and status. |
| 401 | Missing or invalid API key / JWT. |
| 402 | Insufficient credits — balance < effective_batch × credits_per_image. |
| 422 | Invalid 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. |
| 500 | Failed to enqueue tasks. |
Jobs are asynchronous. Poll GET /v1/jobs/{job_id}/result until you receive 200 OK. While processing, the endpoint returns 202 Accepted.
| Status | /result returns | Meaning |
|---|---|---|
queued | 202 | Waiting for a worker — keep polling. |
running | 202 | Worker is generating candidates — keep polling. |
succeeded | 200 | All tasks done; result available. |
failed | 200 | Platform error; credits refunded automatically. |
cancelled | 200 | Job cancelled — terminal, will never proceed. Returns 200 (not 202) so clients stop polling. |
| When | Field to use |
|---|---|
| Job is running | preview_best_url — rolling best-so-far, non-null while running. |
Job succeeded and selection_finalized = true | best_result_url — authoritative Top Pick. Use this in production. |
| After finalization, style preference | aesthetic_best_url — highest aesthetic-score candidate. |
| All accepted candidates | result_urls — array of all quality-passed image URLs. |
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.
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/v1/jobs/{job_id}/resultLightweight polling. Returns 200 when finalized./v1/jobs/{job_id}Full job object — includes selection_finalized, preview_best_url, task_progress./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.
{
"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
}{
"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"
}
]
}| Field | Notes |
|---|---|
input_mode | single (classic) or multi (items-based). |
prompt_count | Number of distinct prompts submitted. Always 1 for single-mode. |
notify_on_complete | Whether a completion email was requested. One email per job — not per item. |
items | Per-item results. null for single-mode jobs. Authoritative source for multi-mode — use this over best_result_url when input_mode="multi". |
best_result_url | Authoritative Top Pick for single-mode. May be null for multi-mode — use items[].result_url instead. |
result_urls | All accepted candidate URLs (single-mode). Empty for multi-mode — use items[].result_url. |
accepted_count | Number of quality-passed images. Authoritative. |
quality_score | Best quality score. null for in-progress or all-failed jobs. |
quality_passed | true if at least one image passed the quality gate. |
execution_time_ms | Wall-clock ms from started_at to finished_at. |
items[] entry fields (multi-mode only):
| Field | Notes |
|---|---|
task_index | 0-based index matching submission order. |
prompt | The prompt for this specific item. |
status | queued · running · succeeded · failed |
result_url | Image URL. null if the task failed or files have expired. |
seed | Seed used for this item's generation. |
error_message | Failure reason. null for successful tasks. |
/v1/jobs/{job_id}Full job object — includes selection_finalized, preview_best_url, task_progress.{
"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
}| Field | Notes |
|---|---|
selection_finalized | false while running. true once all tasks done and best result selected. Check this before using best_result_url. |
best_result_url | Authoritative Top Pick. Null until selection_finalized = true. Use this in production (single-mode). |
preview_best_url | Rolling best-so-far during generation. Non-null only while running; null after finalization. |
aesthetic_best_url | Highest aesthetic-score candidate. Non-null only after finalization. |
result_urls | All accepted candidate image URLs (single-mode). |
is_best_effort | true when job succeeded but no images passed quality — images delivered below preferred threshold, no refund. |
task_progress | Array of per-task progress objects. Only present when status = running. |
input_mode | single or multi. |
items | Per-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_passed | is_best_effort | Meaning |
|---|---|---|
| true | false | Normal delivery — at least one image passed the quality filter. |
| false | true | Best-effort delivery — images generated but none passed quality threshold. Best available image delivered. Credits not refunded. |
| false | false | Platform failure — no images generated at all. Credits refunded automatically. |
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.
| Scenario | Credits |
|---|---|
| Job created | Deducted 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 submission | No charge — rejected before any work begins |
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.
Purchase via Dashboard → Billing, or POST /v1/billing/checkout (Bearer JWT).
All error responses return a JSON object with a detail field explaining the reason.
| Code | detail | Cause |
|---|---|---|
| 401 | Invalid or inactive API key | API key revoked, expired, or malformed. |
| 401 | Invalid or expired token | Bearer JWT expired or invalid. |
| 402 | Not enough credits. Purchase more to continue. | Credit balance below job cost. |
| 403 | Access denied | JWT user or customer key accessing a job they don't own. |
| 404 | Job not found | Job ID does not exist or belongs to another account. |
| 409 | Cannot select a candidate while job is '...' | Job not yet completed, or no accepted candidates. |
| 422 | This 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. |
| 500 | Failed to create job | Platform error — safe to retry. |
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.
| Endpoint | Limit |
|---|---|
POST /v1/jobs | 20 req / min |
GET /v1/jobs/{id} | 120 req / min |
GET /v1/jobs/{id}/result | 120 req / min |
POST /v1/jobs/{id}/select | 30 req / min |
POST /v1/billing/checkout | 5 req / min |
Generated image files are stored for 30 days after job creation. Job metadata (prompt, status, accepted count, credits) is retained indefinitely.
| storage_state | Meaning | URLs returned? |
|---|---|---|
active | Files available in storage. | Yes |
pending_delete | Scheduled for deletion in next cleanup run. | Yes — download now |
deleted | Files removed; only metadata remains. | No — all URLs null |
delete_failed | Storage deletion failed; will retry. | Yes |
| Field | Notes |
|---|---|
files_expire_at | ISO timestamp — when files are scheduled for deletion. |
storage_state | One of: active, pending_delete, deleted, delete_failed. |
files_deleted_at | Populated when cleanup marks the job deleted. |