# 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.

```json
{ "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](#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](https://docs.refabric.com/task-apis/calling-tasks/asynchronous-jobs#result). 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](https://docs.refabric.com/task-apis/errors/task-errors#warnings)). `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:

```json
{ "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`:

```json
{ "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](#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

```bash
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.

```json
{ "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)).

## 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](#products-your-uploads) answers with.

```bash
# 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 "file=@shirt.jpg" -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`.

```json
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](https://docs.refabric.com/task-apis/limits).

Media fields also take a public `https` url of another site directly — no `POST /v1/files` first
([Reuse](#reuse-pass-a-file-back), [Limits](https://docs.refabric.com/task-apis/limits)). Uploading first works too, and gives the
file its id before any job uses it.

```python
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

```bash
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`) |

```json
{ "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](#library-items), 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`](#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:

```bash
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.
