FLAM

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

InNameTypeRequiredNotes
queryfolderIdstringnoNarrow to one collection

Responses

StatusMeaning
200Asset list
401No 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-staging.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

InNameTypeRequiredNotes
pathidstringyes

Responses

StatusMeaning
200The image bytes (content-type from the asset row)
401No valid session
403Not the owner
404Unknown or deleted

Call it

curl -X GET "https://api-staging.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

InNameTypeRequiredNotes
pathidstringyes

Request bodyapplication/json (required)

FieldTypeRequiredNotes
namestringno
skustring | nullno
kind"clothes" | "accessory" | "jewelry" | "model" | "background"no
folderIdstring | nullno
fit"auto" | "fitted" | "regular" | "loose" | "oversized"noHow the garment is cut to be worn. Stored as the reserved fit: tag; null clears it back to unread.
wearer"women" | "men" | "unisex"noThe 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

StatusMeaning
200The updated asset
400BAD_BODY / BAD_KIND / BAD_SKU / BAD_FOLDER
401No valid session
404Unknown 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-staging.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

InNameTypeRequiredNotes
pathidstringyes

Responses

StatusMeaning
200The image bytes (Cache-Control private, max-age=3600; ETag from R2)
401No valid session
404Unknown, deleted, foreign, or missing in R2

Call it

curl -X GET "https://api-staging.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 bodyapplication/json

FieldTypeRequiredNotes
assetIdsstring[]yes
folderIdstring | nullno
kindstring | nullno
approvedbooleanno
{
  "assetIds": [
    "string"
  ],
  "folderId": "string",
  "kind": "string",
  "approved": true
}

Responses

StatusMeaning
200How many pieces were re-filed
400BAD_KIND, or none of folderId / kind / approved was given
401Unauthenticated
404The target collection is not this house's

200 returns:

{
  "updated": 0
}

Call it

curl -X PATCH "https://api-staging.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 bodyapplication/json

FieldTypeRequiredNotes
assetIdsstring[]yes
{
  "assetIds": [
    "string"
  ]
}

Responses

StatusMeaning
200How many pieces were taken out
401Unauthenticated
403A viewer cannot take pieces out

200 returns:

{
  "deleted": 0
}

Call it

curl -X POST "https://api-staging.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 bodyapplication/json

FieldTypeRequiredNotes
assetIdsstring[]yes
operation"remove-background" | "packshot"yes
quality"1k" | "2k" | "4k"no
alsoRemoveBackgroundbooleanno
{
  "assetIds": [
    "string"
  ],
  "operation": "remove-background",
  "quality": "1k",
  "alsoRemoveBackground": true
}

Responses

StatusMeaning
200The whole selection's price, before anything runs
401Unauthenticated

200 returns:

{
  "operation": "string",
  "items": 0,
  "perItem": 0,
  "tokens": 0,
  "spendable": 0,
  "affordable": true
}

Call it

curl -X POST "https://api-staging.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 bodyapplication/json (required)

FieldTypeRequiredNotes
assetIdsstring[]yesUnique, non-empty. Duplicates are rejected.
namestringnoAccepted and currently unused
{
  "assetIds": [
    "string"
  ],
  "name": "string"
}

Responses

StatusMeaning
201The new group id
400BAD_BODY / BAD_ASSET_IDS
401No valid session
404One of the ids is unknown, deleted or foreign — nothing was written

201 returns:

{
  "groupId": "string"
}

Call it

curl -X POST "https://api-staging.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 bodyapplication/json

FieldTypeRequiredNotes
assetIdsstring[]yes
namestringno
sizesobject[]no
{
  "assetIds": [
    "string"
  ],
  "name": "string",
  "sizes": [
    {
      "name": "string",
      "width": 0,
      "height": 0
    }
  ]
}

Responses

StatusMeaning
200Ready now — fetch href
202Packing in the background; the bar will say when
401Unauthenticated
404None of those ids are this house's
413Past what one pack can carry — split the selection
503Sizes 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-staging.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 bodymultipart/form-data (required)

FieldTypeRequiredNotes
filefileyes
folderIdstringnoTarget collection; omit for the root
namestringnoYour own label; wins over the smart read
kind"clothes" | "accessory" | "jewelry" | "model" | "background"noYour own wardrobe bucket; wins over the smart read
skustringnoThe brand's own code
classify"1" | "true" | "on" | "0"noOpt in to the 1-token smart read

Responses

StatusMeaning
201The hung asset (+ classify when the read was requested)
400NO_FILE / UNSUPPORTED_TYPE / EMPTY_FILE / BAD_KIND / BAD_SKU / BAD_FORM
401No valid session
404Unknown or foreign folderId
413IMAGE_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-staging.flam.fashion/api/toolkit/assets/upload" \
  -H "Authorization: Bearer $FLAM_API_KEY" \
  -F "[email protected]" \
  -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

StatusMeaning
200Grounds + the house's own sets
401No 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-staging.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

StatusMeaning
200The folder list
401No 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-staging.flam.fashion/api/toolkit/folders" \
  -H "Authorization: Bearer $FLAM_API_KEY"

POST /api/toolkit/folders

Create a collection

Request bodyapplication/json (required)

FieldTypeRequiredNotes
namestringyes
parentIdstring | nullnoNest under an owned folder
{
  "name": "string",
  "parentId": "string"
}

Responses

StatusMeaning
201The created folder
400BAD_BODY / NAME_REQUIRED
401No valid session
404Unknown 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-staging.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

InNameTypeRequiredNotes
pathidstringyes

Request bodyapplication/json (required)

FieldTypeRequiredNotes
namestringno
{
  "name": "string"
}

Responses

StatusMeaning
200The folder
400BAD_BODY
401No valid session
404Unknown 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-staging.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

InNameTypeRequiredNotes
pathidstringyes

Responses

StatusMeaning
200The pack
401Unauthenticated
404No 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-staging.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

InNameTypeRequiredNotes
pathidstringyes
headerRangestringnobytes=N- / bytes=N-M. Honoured on the background tier only.

Responses

StatusMeaning
200application/zip, Content-Disposition: attachment
206A byte range of the archive
401No valid session
404Unknown, foreign, or the object is gone from R2
409Still packing, or the pack failed
410The link has expired

Call it

curl -X GET "https://api-staging.flam.fashion/api/toolkit/packs/{id}/file" \
  -H "Authorization: Bearer $FLAM_API_KEY"