Assets
16 Assets routes on the FLAM API: The house's captured assets, newest first; Stream one asset's bytes from R2 (owner-gated); Rename, classify, tag or.
Base URL https://api.flam.fashion. Send Authorization: Bearer flam_sk_… on every call; a handful of routes are session-only and say so. How keys and roles work.
GET /api/toolkit/assets
The house's captured assets, newest first
The body is { assets: [...] }, not a bare array — it was documented as a bare array until task #40 and never was one.
Parameters
| In | Name | Type | Required | Notes |
|---|---|---|---|---|
| query | folderId | string | no | Narrow to one collection |
Responses
| Status | Meaning |
|---|---|
200 | Asset list |
401 | No valid session |
200 returns:
{
"assets": [
{
"id": "string",
"name": "string",
"sku": "string",
"kind": "clothes",
"url": "string",
"folderId": "string",
"groupId": "string",
"fit": "auto",
"wearer": "women",
"fitGuessed": true,
"wearerGuessed": true,
"carriesMark": true,
"markGuessed": true,
"mime": "string",
"tool": "string",
"createdAt": "2026-07-27T09:00:00.000Z"
}
]
}Call it
curl -X GET "https://api.flam.fashion/api/toolkit/assets" \
-H "Authorization: Bearer $FLAM_API_KEY"GET /api/toolkit/assets/{id}
Stream one asset's bytes from R2 (owner-gated)
Parameters
| In | Name | Type | Required | Notes |
|---|---|---|---|---|
| path | id | string | yes | — |
Responses
| Status | Meaning |
|---|---|
200 | The image bytes (content-type from the asset row) |
401 | No valid session |
403 | Not the owner |
404 | Unknown or deleted |
Call it
curl -X GET "https://api.flam.fashion/api/toolkit/assets/{id}" \
-H "Authorization: Bearer $FLAM_API_KEY"PATCH /api/toolkit/assets/{id}
Rename, classify, tag or move one piece
An explicit folderId: null moves the piece back to the root. kind is stored as the reserved kind: tag and sku as the sku: tag, so both can be cleared with null. An empty patch is a 200 no-op, not an error.
Parameters
| In | Name | Type | Required | Notes |
|---|---|---|---|---|
| path | id | string | yes | — |
Request body — application/json (required)
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | no | — |
sku | string | null | no | — |
kind | "clothes" | "accessory" | "jewelry" | "model" | "background" | no | — |
folderId | string | null | no | — |
fit | "auto" | "fitted" | "regular" | "loose" | "oversized" | no | How the garment is cut to be worn. Stored as the reserved fit: tag; null clears it back to unread. |
wearer | "women" | "men" | "unisex" | no | The body the garment is CUT FOR — not the body a packshot's ghost form implies. Stored as the reserved wearer: tag; null clears it. unisex is a merchandising fact only a human can state, so the house never guesses it. |
{
"name": "string",
"sku": "string",
"kind": "clothes",
"folderId": "string",
"fit": "auto",
"wearer": "women"
}Responses
| Status | Meaning |
|---|---|
200 | The updated asset |
400 | BAD_BODY / BAD_KIND / BAD_SKU / BAD_FOLDER |
401 | No valid session |
404 | Unknown or foreign asset, or unknown target folder |
200 returns:
{
"asset": {
"id": "string",
"name": "string",
"sku": "string",
"kind": "clothes",
"url": "string",
"folderId": "string",
"groupId": "string",
"fit": "auto",
"wearer": "women",
"fitGuessed": true,
"wearerGuessed": true,
"carriesMark": true,
"markGuessed": true,
"mime": "string",
"tool": "string",
"createdAt": "2026-07-27T09:00:00.000Z"
}
}Call it
curl -X PATCH "https://api.flam.fashion/api/toolkit/assets/{id}" \
-H "Authorization: Bearer $FLAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"string","sku":"string","kind":"clothes","folderId":"string","fit":"auto","wearer":"women"}'GET /api/toolkit/assets/{id}/file
Stream one asset's bytes (the contract serving route)
Where every url in a list response points. Owner-gated, and a FOREIGN id answers 404, never 403 — this route never confirms that someone else's asset exists. (The legacy /api/toolkit/assets/{id} answers 403 for a foreign id; it is kept only for old links.)
Parameters
| In | Name | Type | Required | Notes |
|---|---|---|---|---|
| path | id | string | yes | — |
Responses
| Status | Meaning |
|---|---|
200 | The image bytes (Cache-Control private, max-age=3600; ETag from R2) |
401 | No valid session |
404 | Unknown, deleted, foreign, or missing in R2 |
Call it
curl -X GET "https://api.flam.fashion/api/toolkit/assets/{id}/file" \
-H "Authorization: Bearer $FLAM_API_KEY"PATCH /api/toolkit/assets/batch
Move or re-file many pieces at once
{ assetIds, folderId?, kind? } — the batch twin of PATCH /api/toolkit/assets/:id, minus name and sku (those are one piece's own words and have no batch meaning).
folderId: null moves the whole selection back to the root. kind: null un-files them.
updated is the number the server actually touched — ids this house does not own, or that were already taken out, are skipped rather than failing the batch.
approved (spec 021) is THE DIRECTOR'S DECISION on a selection: true stamps her name and the time on every frame, false writes both back to null. This is the door the one-tap adopt offer rides — a returned share's keeps become a PRE-FILLED decision she confirms, never an automation. The house never sets this by itself.
Request body — application/json
| Field | Type | Required | Notes |
|---|---|---|---|
assetIds | string[] | yes | — |
folderId | string | null | no | — |
kind | string | null | no | — |
approved | boolean | no | — |
{
"assetIds": [
"string"
],
"folderId": "string",
"kind": "string",
"approved": true
}Responses
| Status | Meaning |
|---|---|
200 | How many pieces were re-filed |
400 | BAD_KIND, or none of folderId / kind / approved was given |
401 | Unauthenticated |
404 | The target collection is not this house's |
200 returns:
{
"updated": 0
}Call it
curl -X PATCH "https://api.flam.fashion/api/toolkit/assets/batch" \
-H "Authorization: Bearer $FLAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"assetIds":["string"],"folderId":"string","kind":"string","approved":true}'POST /api/toolkit/assets/batch/delete
Take many pieces out of the library
{ assetIds } — a SOFT take-out. assets.deleted_at is stamped, which every read path in the service already filters on; the row stays, the R2 object stays, nothing is destroyed.
deleted is counted SERVER-SIDE from the ids this house actually owns and had not already taken out. The client's own count is never used for anything.
A viewer may look at the library and may not empty it.
Request body — application/json
| Field | Type | Required | Notes |
|---|---|---|---|
assetIds | string[] | yes | — |
{
"assetIds": [
"string"
]
}Responses
| Status | Meaning |
|---|---|
200 | How many pieces were taken out |
401 | Unauthenticated |
403 | A viewer cannot take pieces out |
200 returns:
{
"deleted": 0
}Call it
curl -X POST "https://api.flam.fashion/api/toolkit/assets/batch/delete" \
-H "Authorization: Bearer $FLAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"assetIds":["string"]}'POST /api/toolkit/assets/batch/quote
What would this cost, for all of them
{ assetIds, operation } → one summed quote for the whole selection, spending nothing and reserving nothing. items is how many of the ids this house really holds; tokens is the price book's per-piece price times that number; affordable compares it to the house's spendable balance.
operation is remove-background or packshot.
quality is optional and only read for packshot — it prices the batch at that resolution, so a size control on the calling surface can move the number it charges. Omit it and the batch prices at the house's default.
Request body — application/json
| Field | Type | Required | Notes |
|---|---|---|---|
assetIds | string[] | yes | — |
operation | "remove-background" | "packshot" | yes | — |
quality | "1k" | "2k" | "4k" | no | — |
alsoRemoveBackground | boolean | no | — |
{
"assetIds": [
"string"
],
"operation": "remove-background",
"quality": "1k",
"alsoRemoveBackground": true
}Responses
| Status | Meaning |
|---|---|
200 | The whole selection's price, before anything runs |
401 | Unauthenticated |
200 returns:
{
"operation": "string",
"items": 0,
"perItem": 0,
"tokens": 0,
"spendable": 0,
"affordable": true
}Call it
curl -X POST "https://api.flam.fashion/api/toolkit/assets/batch/quote" \
-H "Authorization: Bearer $FLAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"assetIds":["string"],"operation":"remove-background","quality":"1k","alsoRemoveBackground":true}'POST /api/toolkit/assets/group
Stamp a fresh shared group onto a set of pieces
Writes group:<uuid> onto every named asset, REPLACING any prior group (re-grouping moves a piece). All-or-nothing: every id must be one of the caller's live assets or nothing is written.
Request body — application/json (required)
| Field | Type | Required | Notes |
|---|---|---|---|
assetIds | string[] | yes | Unique, non-empty. Duplicates are rejected. |
name | string | no | Accepted and currently unused |
{
"assetIds": [
"string"
],
"name": "string"
}Responses
| Status | Meaning |
|---|---|
201 | The new group id |
400 | BAD_BODY / BAD_ASSET_IDS |
401 | No valid session |
404 | One of the ids is unknown, deleted or foreign — nothing was written |
201 returns:
{
"groupId": "string"
}Call it
curl -X POST "https://api.flam.fashion/api/toolkit/assets/group" \
-H "Authorization: Bearer $FLAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"assetIds":["string"],"name":"string"}'POST /api/toolkit/assets/pack
Take a whole selection as one file
{ assetIds } → one archive of the frames this house really owns, at any size.
The server decides the tier and SAYS which, so the caller never has to. Under 250 frames AND 256 MiB the archive is assembled inside the request and status comes back ready — fetch href and the download starts at once. Above either, mode is pack, status is queued, seconds is roughly how long it will take, and a pack.ready notification arrives on the existing socket when the archive is up. Either way href is the same URL and the link lives 7 days.
Refused above 700 frames or 4 GiB — the ceiling a single background invocation fits. Split the selection.
FREE. Nothing here reads the price book and nothing here reserves tokens: these are her own files leaving her own library.
Request body — application/json
| Field | Type | Required | Notes |
|---|---|---|---|
assetIds | string[] | yes | — |
name | string | no | — |
sizes | object[] | no | — |
{
"assetIds": [
"string"
],
"name": "string",
"sizes": [
{
"name": "string",
"width": 0,
"height": 0
}
]
}Responses
| Status | Meaning |
|---|---|
200 | Ready now — fetch href |
202 | Packing in the background; the bar will say when |
401 | Unauthenticated |
404 | None of those ids are this house's |
413 | Past what one pack can carry — split the selection |
503 | Sizes were asked for and this tier cannot cut them |
200 returns:
{
"packId": "string",
"mode": "instant",
"status": "queued",
"items": 0,
"bytes": 0,
"href": "string",
"filename": "string",
"seconds": 0,
"error": "string"
}Call it
curl -X POST "https://api.flam.fashion/api/toolkit/assets/pack" \
-H "Authorization: Bearer $FLAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"assetIds":["string"],"name":"string","sizes":[{"name":"string","width":0,"height":0}]}'POST /api/toolkit/assets/upload
Hang one image in the house library (multipart)
Bytes → R2, row → assets. The upload itself is FREE.
SMART UPLOAD — classify=1 is an OPT-IN read priced at 1 token per image (the describe-item classifier, run in-process after the row is written). It files the piece into its wardrobe bucket (kind) and gives it a human name — "IMG_2489.jpeg" becomes "white ribbed lace trim henley", a model collage comes back kind=model. Anything you state yourself wins: a supplied name/kind is never overwritten, and sku is your own code — it is never invented. Omit classify and nothing is read and nothing is charged.
A failed read never loses the upload and never charges: 201 with the asset unclassified and classify.ok=false carrying the reason (INSUFFICIENT_TOKENS, AI_NOT_CONFIGURED, AI_FAILED, …).
Request body — multipart/form-data (required)
| Field | Type | Required | Notes |
|---|---|---|---|
file | file | yes | — |
folderId | string | no | Target collection; omit for the root |
name | string | no | Your own label; wins over the smart read |
kind | "clothes" | "accessory" | "jewelry" | "model" | "background" | no | Your own wardrobe bucket; wins over the smart read |
sku | string | no | The brand's own code |
classify | "1" | "true" | "on" | "0" | no | Opt in to the 1-token smart read |
Responses
| Status | Meaning |
|---|---|
201 | The hung asset (+ classify when the read was requested) |
400 | NO_FILE / UNSUPPORTED_TYPE / EMPTY_FILE / BAD_KIND / BAD_SKU / BAD_FORM |
401 | No valid session |
404 | Unknown or foreign folderId |
413 | IMAGE_TOO_LARGE (12 MB cap) |
201 returns:
{
"asset": {
"id": "string",
"name": "string",
"sku": "string",
"kind": "clothes",
"url": "string",
"folderId": "string",
"groupId": "string",
"fit": "auto",
"wearer": "women",
"fitGuessed": true,
"wearerGuessed": true,
"carriesMark": true,
"markGuessed": true,
"mime": "string",
"tool": "string",
"createdAt": "2026-07-27T09:00:00.000Z"
},
"classify": {
"ok": true,
"nameEn": "string",
"kind": "string",
"error": "string",
"tokens": 0
}
}Call it
curl -X POST "https://api.flam.fashion/api/toolkit/assets/upload" \
-H "Authorization: Bearer $FLAM_API_KEY" \
-F "file=@file.png" \
-F "folderId=<folderId>" \
-F "name=<name>" \
-F "kind=<kind>" \
-F "sku=<sku>" \
-F "classify=<classify>"GET /api/toolkit/backgrounds
The whole library a look can stand on
Two sources, one call — the house's curated grounds (static, shipped with the code) and this director's own scenes (assets tagged kind:background, uploaded through /api/toolkit/assets/upload). Only the PUBLIC projection of a ground crosses the wire; the prompt clause it contributes stays server-side.
Responses
| Status | Meaning |
|---|---|
200 | Grounds + the house's own sets |
401 | No valid session |
200 returns:
{
"grounds": [
{
"id": "string",
"kind": "string",
"name": "string",
"note": "string",
"tone": "string",
"previewUrl": "string"
}
],
"scenes": [
{
"id": "string",
"name": "string",
"url": "string",
"createdAt": "2026-07-27T09:00:00.000Z"
}
]
}Call it
curl -X GET "https://api.flam.fashion/api/toolkit/backgrounds" \
-H "Authorization: Bearer $FLAM_API_KEY"GET /api/toolkit/folders
Every collection in the house, flat
Oldest first. The client builds the tree from parentId.
Responses
| Status | Meaning |
|---|---|
200 | The folder list |
401 | No valid session |
200 returns:
{
"folders": [
{
"id": "string",
"name": "string",
"parentId": "string",
"createdAt": "2026-07-27T09:00:00.000Z"
}
]
}Call it
curl -X GET "https://api.flam.fashion/api/toolkit/folders" \
-H "Authorization: Bearer $FLAM_API_KEY"POST /api/toolkit/folders
Create a collection
Request body — application/json (required)
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | — |
parentId | string | null | no | Nest under an owned folder |
{
"name": "string",
"parentId": "string"
}Responses
| Status | Meaning |
|---|---|
201 | The created folder |
400 | BAD_BODY / NAME_REQUIRED |
401 | No valid session |
404 | Unknown or foreign parentId |
201 returns:
{
"folder": {
"id": "string",
"name": "string",
"parentId": "string",
"createdAt": "2026-07-27T09:00:00.000Z"
}
}Call it
curl -X POST "https://api.flam.fashion/api/toolkit/folders" \
-H "Authorization: Bearer $FLAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"string","parentId":"string"}'PATCH /api/toolkit/folders/{id}
Rename a collection
A no-op rename (same name, or none supplied) still answers 200 with the folder.
Parameters
| In | Name | Type | Required | Notes |
|---|---|---|---|---|
| path | id | string | yes | — |
Request body — application/json (required)
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | no | — |
{
"name": "string"
}Responses
| Status | Meaning |
|---|---|
200 | The folder |
400 | BAD_BODY |
401 | No valid session |
404 | Unknown or foreign id |
200 returns:
{
"folder": {
"id": "string",
"name": "string",
"parentId": "string",
"createdAt": "2026-07-27T09:00:00.000Z"
}
}Call it
curl -X PATCH "https://api.flam.fashion/api/toolkit/folders/{id}" \
-H "Authorization: Bearer $FLAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"string"}'GET /api/toolkit/packs/{id}
Where a pack got to
The refresh/reconnect read. The LIVE path is the socket — a pack.ready notification arrives the moment the archive is up, and nothing in this house polls this route on a timer (CLAUDE.md: push, don't poll). It exists for the tab that was closed while the pack was building and the bar line that is opened tomorrow.
Parameters
| In | Name | Type | Required | Notes |
|---|---|---|---|---|
| path | id | string | yes | — |
Responses
| Status | Meaning |
|---|---|
200 | The pack |
401 | Unauthenticated |
404 | No such pack in this house |
200 returns:
{
"packId": "string",
"mode": "instant",
"status": "queued",
"items": 0,
"bytes": 0,
"href": "string",
"filename": "string",
"seconds": 0,
"error": "string"
}Call it
curl -X GET "https://api.flam.fashion/api/toolkit/packs/{id}" \
-H "Authorization: Bearer $FLAM_API_KEY"GET /api/toolkit/packs/{id}/file
Stream a batch download's archive
The ONE download URL a pack answers with, on both tiers — see POST /api/toolkit/assets/pack, which is where an id comes from.
Under the instant threshold nothing was ever stored: the zip is assembled from R2 inside this request and streamed, chunked, with no Content-Length. Above it, this hands back the archive the background job built, and honours Range so a dropped 3 GB download resumes instead of starting over.
A foreign or unknown id answers 404 and never 403 — the same precedent as the single-asset byte route. 409 means the pack is still building (the bar will say when — nothing here is meant to be polled) or it failed. 410 means the seven-day link has expired.
Parameters
| In | Name | Type | Required | Notes |
|---|---|---|---|---|
| path | id | string | yes | — |
| header | Range | string | no | bytes=N- / bytes=N-M. Honoured on the background tier only. |
Responses
| Status | Meaning |
|---|---|
200 | application/zip, Content-Disposition: attachment |
206 | A byte range of the archive |
401 | No valid session |
404 | Unknown, foreign, or the object is gone from R2 |
409 | Still packing, or the pack failed |
410 | The link has expired |
Call it
curl -X GET "https://api.flam.fashion/api/toolkit/packs/{id}/file" \
-H "Authorization: Bearer $FLAM_API_KEY"