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" }
FieldMeaning
filethe 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.
urla 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_typestandard MIME type (image/png, image/svg+xml, application/pdf, application/json, …)
taskthe task that produced it (null for uploads) — its outputSchema says what the file is
job_idthe job that produced it (null for uploads)
created_atISO-8601 UTC
dataa 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

KindThe content isExamples
mediathe bytes at urlimages, video, SVG, PDF
structured resultthe data object (url is a preview — a cover image or PDF)moodboard, fabric analysis, range plan, brand kit
library itemthe image at url — a pose or a backdrop in your library, with an idpose:…, 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 fieldMeaning
namethe product's name
external_idyour own id for it (SKU)
descriptionthe fit note: how it fits or should sit, used as a soft hint
categorywhat the product is — one of the values listed at GET /v1/vocab/garment_type, the same word an upload takes
itemsits 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.

typeviewWhat it isWhere it comes from
producta side other than the frontyour own photo of that side (the front is the file itself)your upload
label—a photo of its labelyour upload
detailoptionala close photo of a detail (for the view, when said)your upload
ghosta sidethe product on an invisible mannequinmade by a shoot, saved under the product
flata sidethe product laid flatmade by a shoot, saved under the product
close_up—a close-up of the fabric and finishmade 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 partthe file: a public https address (JSON), or its bytes (multipart)
nameoptional — the product's name
external_idoptional — your own id for it (SKU); a number is taken as text
descriptionoptional — a fit note
categoryoptional — 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
itemsoptional — 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 https addresses — http:// answers 422 invalid_request naming 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.upload of GET /v1/meta; the type is read from the file's bytes, not from the declared Content-Type. A file of another type answers 415 unsupported_media_type, a larger file 413 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.

KindRow
file (a product)file, name, url, external_id
modelfile, name, url, gender (who is pictured); base for a style — the base model's model:…
posefile, url, gender (who is pictured)
backgroundfile, 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 file reference, not the url (file and url): the file is 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.