For AI agents: this page is also available as Markdown at https://docs.refabric.com/task-apis/files-and-media.md, and the index of every page is https://docs.refabric.com/llms.txt.
Task APIs
Files
The one shape every uploaded and delivered file has, and how to upload, reuse, list and download files.
Every input you upload and every output a job produces is a file. A file has one shape, whatever it contains.
{ "file": "art:x1", "url": "https://…", "media_type": "image/png",
"task": "image.glam", "job_id": "j_…", "created_at": "2026-09-28T10:01:10Z" }| Field | Meaning |
|---|---|
file | the file's stable id — an opaque string (art:… for produced files, file:… for uploads, a record handle such as moodboard:…, a library handle such as pose:…). Store it and pass it back as input. |
url | a public download link (not signed, no expiry). Use it to download or show the file; re-read the file (GET /v1/files/{ref}) for its current url. Never store it as the file's identity — see Download URLs. |
media_type | standard MIME type (image/png, image/svg+xml, application/pdf, application/json, …) |
task | the task that produced it (null for uploads) — its outputSchema says what the file is |
job_id | the job that produced it (null for uploads) |
created_at | ISO-8601 UTC |
data | a structured result: its content (a media file may carry extra facts here too) |
The same object answers everywhere a file appears — GET /v1/files/{ref}, a job's files[], a
webhook, a Prefer: wait answer.
file and url
Every answer carries both, always. file is what you keep: it never changes and it is what every
input takes. url is for fetching the bytes: it is the file's current public link, and it can
change (trial accounts receive watermarked images, a plan change switches it, a file can be re-hosted)
— the stored file does not. Sending a file back as file (or as the url we gave you)
keeps its context — what made it, its model, where it belongs — so an edit of it knows what it
edits; any other image starts fresh.
In a job result
A job's files[] lists only that job's files, in the order the task documents for
its outputs (e.g. image.rotate_views back · left · right), paged (limit's default and maximum
are in the OpenAPI spec; has_more and next_cursor read the next page) — Jobs. An output the job could not make is not a file: it is
a summary.warnings[] row output_not_produced whose field names it
(Errors). summary is present only when the offer has counts or warnings to
say; a single-file offer's result has no summary key. A job that makes a record lists only the record's file; the
record's images are its data.items[].url.
Two kinds
| Kind | The content is | Examples |
|---|---|---|
| media | the bytes at url | images, video, SVG, PDF |
| structured result | the data object (url is a preview — a cover image or PDF) | moodboard, fabric analysis, range plan, brand kit |
| library item | the image at url — a pose or a backdrop in your library, with an id | pose:…, background:… |
A structured result's url is a preview, never its content: the cover image of a moodboard, brand
kit, range plan, fabric or photoshoot record, and for image.extract_materials the analysed image — the
content is data.
Images inside a structured result (swatches, colour variants, rendered cells) are its
data.items[].url — not separate files. Pass such a url straight into any image field. A job that
makes a structured result lists only that result's file (one file, one page), never its parts.
Library items
pose.create and background.create make one library item each, and their job lists only its
line — the same object GET /v1/files/{pose:… | background:…} answers:
{ "file": "pose:7c1e…", "url": "https://…", "media_type": "image/jpeg",
"task": "pose.create", "job_id": "7c1e…", "created_at": "2026-10-01T09:12:04Z" }url is the item's image (a backdrop with any person taken out), task the offer that makes the
kind, and there is no data. job_id names the job that made it when that is
known: a pose you made names its job; job_id is null for a background. Pass pose:… to poses (image.repose, photoshoot.create) and
background:… to background (image.change_background, the shoots).
Products (your uploads)
An upload (file:…) is a product. Its read carries what you told us about it, in data:
{ "file": "file:1234", "url": "https://…", "media_type": "image/jpeg",
"task": null, "job_id": null, "created_at": "2026-10-01T09:00:00Z",
"data": { "name": "Linen shirt", "external_id": "SKU-1", "description": "relaxed fit",
"category": "top",
"items": [ { "type": "product", "view": "back", "url": "https://…" },
{ "type": "label", "url": "https://…" },
{ "type": "ghost", "view": "front", "url": "https://…" } ] } }data field | Meaning |
|---|---|
name | the product's name |
external_id | your own id for it (SKU) |
description | the fit note: how it fits or should sit, used as a soft hint |
category | what the product is — one of the values listed at GET /v1/vocab/garment_type, the same word an upload takes |
items | its other images, each {type, view, url} — see the table below. Left out by view=basic |
Empty is absent: a field the product does not have is left out, and an upload you said nothing
about has no data at all.
Items are {type, view, url} — type is what the image is, view which side of the product it
shows (one of the values listed at GET /v1/vocab/view_angle; absent when it has none). Every record item uses the
same two words (a shoot's cells too: {"type": "ghost", "view": "back"}), and a shoot request asks
for its views in them: views: [{"type": "ghost", "view": "back"}, {"type": "close_up"}] — what
you ask for is what the record's items say.
type | view | What it is | Where it comes from |
|---|---|---|---|
product | a side other than the front | your own photo of that side (the front is the file itself) | your upload |
label | — | a photo of its label | your upload |
detail | optional | a close photo of a detail (for the view, when said) | your upload |
ghost | a side | the product on an invisible mannequin | made by a shoot, saved under the product |
flat | a side | the product laid flat | made by a shoot, saved under the product |
close_up | — | a close-up of the fabric and finish | made by a shoot, saved under the product |
The views a shoot saved under a product (its ghost, flat and close_up items; every item type is listed at GET /v1/vocab/item_type) can be reused
by a later photoshoot.create of the same product: ask for the view with reuse: true.
Read one file
curl -s "$REFABRIC_API/files/art:x1?view=full" -H "Authorization: Key $REFABRIC_API_KEY"Returns the file object plus data — the structured content (or extra facts for media).
view=full (default) returns everything; view=basic omits large sections (a record's items). The shape of data
is documented by the producing task's outputSchema.
Reuse: pass a file back
Every media input is one string: a file reference or a URL. There is no object form.
{ "image": "art:x1" }
{ "image": "https://…" }
{ "references": [ { "image": "file:1234", "use_case": "fabric" } ] }What an answer gives can go into a request unchanged: a file line's file, a record's
data.items[].url, a record handle (moodboard:…) where a field takes one. When the file is one we
produced for you, the server knows its context (what made it, where it belongs) and uses it. An image we did not produce simply starts fresh.
A media url is https only (http:// answers 422 invalid_request naming the field). A url of
another site may carry its own query string (https://cdn.example.com/a.jpg?w=800 is that image):
it is fetched when you submit and kept as your own file: — the same file POST /v1/files with
that url gives you — and the job uses that file. A transformed copy of one of OUR files
(…?width=100 on a url we gave you) still answers 422: save it first.
A record you create is described by the same words it answers with: its content is items
([{type, url, name}], as in its data.items), its images are images (a list of those strings).
An upload is too: name, external_id, description, category, items (Upload).
Upload
POST /v1/files gives a file an id you can pass to any task — upload, read (GET /v1/files/{ref})
and delete (DELETE /v1/files/{ref}) are one collection. Two ways in, one answer — and the same
words a product read answers with.
# by URL — a public https address of your file
curl -s -X POST "$REFABRIC_API/files" \
-H "Authorization: Key $REFABRIC_API_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://example.com/shirt.jpg", "name": "Linen shirt", "external_id": "SKU-1",
"description": "relaxed fit", "category": "top",
"items": [{"type": "product", "view": "back", "image": "https://example.com/back.jpg"},
{"type": "label", "image": "https://example.com/label.jpg"}]}'
# by bytes — multipart/form-data, the file in the part named `file`
curl -s -X POST "$REFABRIC_API/files" \
-H "Authorization: Key $REFABRIC_API_KEY" -F "[email protected]" -F "name=Linen shirt" \
-F 'items=[{"type": "detail", "view": "front", "image": "https://example.com/collar.jpg"}]'| Field | |
|---|---|
url or the file part | the file: a public https address (JSON), or its bytes (multipart) |
name | optional — the product's name |
external_id | optional — your own id for it (SKU); a number is taken as text |
description | optional — a fit note |
category | optional — what the product is: one of the values listed at GET /v1/vocab/garment_type. The shoots read it from the product — how it is worn, framed and restored — so a shoot takes no category of its own |
items | optional — your other photos, [{type, view, image}]. Your own photos only: product with a view other than the front (required; one per side), label (no view), detail (view optional). The values type and view take are listed at GET /v1/vocab/item_type and GET /v1/vocab/view_angle. image is a media value: a public url, a url from one of our answers, file:… or art:… |
A wrong type or view answers 422 invalid_option naming items[<n>].type / .view, a
category outside the vocab 422 invalid_option on category; an image
at a private address 422 image_not_public, a handle that is not yours 404 not_found. In a
multipart body name, external_id, description and category are form fields and items is one form
field holding the JSON text. There is no kind: every upload is a file: (a sent kind is an
unknown field and is ignored). A backdrop is made with background.create, which reads it (its
description, any person taken out, a studio backdrop's colour); a pose with pose.create.
HTTP 201
{ "file": "file:1234", "url": "https://…", "media_type": "image/jpeg",
"task": null, "job_id": null, "created_at": "2026-09-30T13:16:02Z" }The answer is the file line — the same object GET /v1/files/file:1234 answers, with data when
you sent any of the words above. Pass file as any media value. Registering a url that already has
an id, with no words, answers THAT file as it is (idempotent, 201 both times). Sending words
(name, external_id, description, category, items) for a url that already has an id is refused —
409 conflict on url, naming the file: "This url is already file:1234; its details cannot
change here." A file's words are written once, when its id is first given.
- By URL: URLs must be public
httpsaddresses —http://answers422 invalid_requestnaming the field (url,items[n].image); private and internal addresses are refused (image_not_public). - By bytes: the accepted file types and the size cap are
limits.uploadofGET /v1/meta; the type is read from the file's bytes, not from the declaredContent-Type. A file of another type answers415 unsupported_media_type, a larger file413 file_too_large.
More limits: Limits.
Media fields also take a public https url of another site directly — no POST /v1/files first
(Reuse, Limits). Uploading first works too, and gives the
file its id before any job uses it.
with open("fabric.jpg", "rb") as fh:
up = s.post(f"{API}/files", files={"file": fh}).json()
body = {"prompt": "a shirt in this fabric",
"references": [{"image": up["file"], "use_case": "fabric"}]}List uploads and library items
curl -s "$REFABRIC_API/refs/file?limit=50" -H "Authorization: Key $REFABRIC_API_KEY"
curl -s "$REFABRIC_API/refs/pose" -H "Authorization: Key $REFABRIC_API_KEY"GET /v1/refs/{kind} lists your uploads and the library items a task can use (poses, models,
backgrounds, your records; the kinds are listed at GET /v1/vocab/ref_kind). Returns
{items, has_more, next_cursor}: while has_more is true, pass next_cursor back as ?cursor=
for the next page; the last page has has_more: false and no next_cursor, as GET /v1/jobs does. The cursor is opaque and
belongs to the kind and query it came from: one from another kind or another q / base /
curated answers 422 invalid_request with field: "cursor".
Every row is minimal and the same for every kind — file (the handle to pass), name, url (a
preview) — plus only these, each when known. A pose and a background have no name: their row
is the image and its one word.
| Kind | Row |
|---|---|
| file (a product) | file, name, url, external_id |
| model | file, name, url, gender (who is pictured); base for a style — the base model's model:… |
| pose | file, url, gender (who is pictured) |
| background | file, url, kind (what the backdrop is; values at GET /v1/vocab/background_kind) |
{ "items": [ { "file": "model:5501", "name": "Mila · Style 2", "url": "https://…",
"gender": "woman", "base": "model:4412" } ],
"has_more": true, "next_cursor": "AXsiZiI6…" }A model's style is named <base model name> · Style <n>, n its place among that model's styles
by creation. A row's full content is its read: GET /v1/files/{file}.
A model's read (GET /v1/files/model:…) carries only these in data, each when known: name,
description (a style's look in words), gender (who is pictured), base (a style's base model,
model:…). A pose's or a background's read is its library line, with no data.
Download URLs
- A download URL is the file's public CDN url. It is not signed and does not expire: anyone who has the url can fetch the bytes, so treat it like the file itself.
- Trial accounts receive watermarked images. The watermark is chosen when the url is read, so the same file can answer a different url after a plan change.
- Store the
filereference, not the url (fileandurl): thefileis the stable id, while a url can change (watermark or plan, re-hosting). Re-read the file or the job result for its current url.
Retention and deletion
- Files are kept until you delete them.
- Delete a produced file (
art:…) at any time:
curl -s -X DELETE "$REFABRIC_API/files/art:x1" -H "Authorization: Key $REFABRIC_API_KEY"The answer is 200 with {"file": "art:x1", "deleted": true}. After it, GET /v1/files/art:x1
answers 404; jobs that made or used it keep their record — a job whose only file you deleted still
answers its result, with files: []. Deleting does
not remove the bytes behind a url you already received. Delete your produced files (art:…) through
the API; an upload, a record or a library item answers 422 file_not_deletable.
To change a file, run a task; the result is a new file.