FLAM

The API

Mint an organization key, send it as a bearer token, and every route answers. An MCP endpoint gives an agent six tools to commission a develop and watch it land. Outbound webhooks are not built.

Two doors are open today. Mint an organization key at POST /api/keys, send it as Authorization: Bearer flam_sk_…, and every route the studio uses answers to it. An agent gets a second door: POST /api/mcp, an MCP server with six tools that commission a develop from asset ids and report it landing.

The API and the MCP server

What does not answer yet

Nothing dispatches outbound webhooks, nothing streams progress, and a key carries a role rather than a permission set. Designed and not built is the full list, so you can cost an integration this week. Everything above it is callable now.

Where it answers

The API is served at https://api.flam.fashion, and every route it serves is written up here rather than somewhere else.

WhereWhat it gives you
The referenceEvery operation, grouped, each with a runnable request
/openapi.jsonThe whole surface as OpenAPI 3.1, on this origin

The reference is generated at build from the API's own description, so it cannot drift from what the service actually serves. Every route added from here on also ships with a runnable request in the repo's collection, and CI refuses one without it. A route nobody can call is a promise nobody can keep.

Mint a key

A key belongs to the house rather than to a person, so taking a seat off the team cannot orphan a running integration. Minting needs a browser session: a key that could mint another key could not be revoked, because rotating the leaked one would leave the second key in place.

curl -X POST https://api.flam.fashion/api/keys \
  -H "Content-Type: application/json" \
  -b "$SESSION_COOKIE" \
  -d '{"name":"nightly packshots","role":"member"}'
{
  "id": "8f2c…",
  "name": "nightly packshots",
  "role": "member",
  "prefix": "flam_sk_Xk4pQ2vB",
  "lastUsedAt": null,
  "revokedAt": null,
  "createdAt": "2026-07-27T08:14:00.000Z",
  "key": "flam_sk_…"
}

key is in that response and in no other. Only its SHA-256 is stored, so a key that is lost is re-minted, never recovered.

RouteWhat it does
POST /api/keysMint. Body is { name, role }; role is viewer or member and defaults to member
GET /api/keysEvery key the house has minted, revoked ones included, never the secret
DELETE /api/keys/{id}Revoke. Answers { "revoked": true, "id": … }, idempotent, 404 for another house's key

Revocation is read from the row on every call, so it takes effect on the very next request. There is no cache to wait out. There is also no screen for this yet: keys are minted, listed and revoked over HTTP.

What a key may do

RoleReadsSpends tokensSpends moneyManages keys or seats
vieweryesnonono
memberyesyesnono

Neither reaches money. No key starts a subscription, buys a top-up or buys from Explore, because real money is owner-only. No key offers a seat either, because that needs an owner or an admin. A key can hold neither role, and that ceiling is a database constraint rather than a check a future route can forget. The worst a leaked key does is spend tokens the house had already bought, so revoke it and carry on.

Send it on any route:

curl https://api.flam.fashion/api/toolkit/assets \
  -H "Authorization: Bearer $FLAM_KEY"

Scope, role and 403s are identical whether a person or an agent asked.

The MCP endpoint

POST /api/mcp speaks JSON-RPC 2.0 over Streamable HTTP: one POST in, one JSON object out. It answers initialize, tools/list, tools/call and ping. A notification, meaning a message with no id, gets 202 and an empty body. GET and DELETE answer 405, which is how a client learns there is no event stream at this URL.

curl -X POST https://api.flam.fashion/api/mcp \
  -H "Authorization: Bearer $FLAM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}'
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": { "listChanged": false } },
    "serverInfo": {
      "name": "flam",
      "title": "FLAM — the fashion house for creative AI",
      "version": "1.0.0"
    },
    "instructions": "Start with list_capabilities and list_assets…"
  }
}

initialize and ping answer without a key, so a client can connect and then tell its person what it needs. Every other method wants the key and answers 401 without one.

The six tools

ToolWhat it doesNeeds
list_capabilitiesEvery develop the house runs, with its token price per resolutionany key
check_balanceSpendable tokens, the grant, top-ups, and what is on holdany key
list_assetsThe library, newest first. folderId, limit (default 50, max 200)any key
start_developCommission one frame from asset idsa member key
get_developOne develop's status, result and settled costany key
list_developsRecent develops, newest first. limit (default 20, max 100), statusany key

Every tools/call comes back as a JSON-RPC result, never a JSON-RPC error, even when the tool refused. Read isError and structuredContent.

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{ "type": "text", "text": "{ … }" }],
    "structuredContent": { "jobId": "…", "status": "queued", "tokensHeld": 13 },
    "isError": false
  }
}

A refusal keeps the same envelope with isError: true and a structuredContent of { "error": "INSUFFICIENT_TOKENS: this develop needs 13, the house has 4" }. That is deliberate. A model should be able to read why it was refused and choose what to do next, rather than have its client swallow a protocol error.

Commission a develop

start_develop takes asset ids, because an agent holds ids rather than bytes. list_assets gives you them, or upload first.

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "start_develop",
    "arguments": {
      "tool": "tryon",
      "inputs": { "model": "<assetId>", "garment": "<assetId>" },
      "quality": "2k",
      "params": { "fit": "auto" },
      "idempotencyKey": "spring-line-014"
    }
  }
}

tool is one of the slugs list_capabilities returns. inputs maps a role to one asset id.

DevelopRoles in inputs
single-image developsimage
tryonmodel, garment
cleanupimage, mask
packshot on your own groundimage, background, with params.bg set to image

One asset per role here, so a try-on through MCP takes one garment. params carries the develop's string options, and anything you put there rides along: send a runId and several frames group into one run in the Darkroom. quality is 1k, 2k or 4k and decides what the frame costs.

{ "jobId": "…", "status": "queued", "tokensHeld": 13, "idempotencyKey": "spring-line-014" }

Tokens are held before anything is queued. The hold settles when the frame lands and is released when it fails, so an interrupted develop costs nothing. Send the same idempotencyKey twice and you get the develop you already have, with tokensHeld: 0, not a second charge.

Refusals are named rather than narrated: UNKNOWN_TOOL, NO_INPUTS, ASSET_NOT_FOUND, IMAGE_TOO_LARGE, FORBIDDEN for a viewer key, INSUFFICIENT_TOKENS, and ENQUEUE_FAILED, which charges nothing and is safe to retry. Switch on the string before the colon.

Watch it land

get_develop with the jobId:

{
  "jobId": "…",
  "tool": "tryon",
  "status": "done",
  "resultUrl": "…",
  "resultAssetId": "…",
  "error": null,
  "tokensEstimated": 13,
  "tokensSettled": 13,
  "createdAt": "2026-07-27T08:14:00.000Z",
  "finishedAt": "2026-07-27T08:14:41.000Z"
}

status is queued, running, done, failed or cancelled. Nothing is pushed over MCP, so poll get_develop, or hold the socket below if your client can.

Putting frames into the library

There is no MCP tool for this, so an agent uploads over HTTP with the same key.

curl -X POST https://api.flam.fashion/api/toolkit/assets/upload \
  -H "Authorization: Bearer $FLAM_KEY" \
  -F "file=@garment.jpg" \
  -F "kind=clothes"

201 with { "asset": { "id": …, … } }, and that id is what start_develop takes. 12 MB per image. kind is clothes, accessory, jewelry, model or background, and folderId files it into a collection. Add classify=1 to have the house read the frame and name it for you, which is a billed read.

Sending the bytes instead

POST /api/toolkit/jobs/submit is the multipart door the studio itself uses. Send files rather than ids.

curl -X POST https://api.flam.fashion/api/toolkit/jobs/submit \
  -H "Authorization: Bearer $FLAM_KEY" \
  -F "tool=tryon" \
  -F "model=@model.jpg" \
  -F "garment=@garment.jpg" \
  -F "quality=2k" \
  -F "idempotencyKey=spring-line-014"

Each file field's name is its role, which is how a develop that reads two images gets them in the right places. Repeat garment to send several. Every other string field becomes one of the develop's options. It answers 202 with { jobId, status: "queued" }.

tool, idempotencyKey and brandId are reserved names and never become options. output is refused outright, because a destination is a lookbook decision rather than a raw develop's. Errors: UNKNOWN_TOOL, NO_IMAGE, EMPTY_IMAGE, IMAGE_TOO_LARGE at 413 with maxMb, BAD_FORM, 403 for a viewer key, and 402 with need and spendable when the balance will not cover the work. On a 402 nothing is created and nothing is charged.

Listening for the result

GET /api/toolkit/jobs/ws upgrades to a WebSocket carrying every state change on the house's develops, and it accepts the same key on the Authorization header. It is addressed per organization rather than per seat, so a run one seat started is watchable from another. An idle client makes no requests and still knows the moment a frame lands.

If your client was away, GET /api/toolkit/jobs?since= replays what it missed. POST /api/toolkit/jobs/:id/cancel holds a run; /duplicate runs it again.

What it costs

Tokens meter developing, and list_capabilities returns the live price book. The number it gives you is the number that settles.

{
  "tools": [{ "tool": "packshot", "prices": [{ "resolution": "2k", "tokens": 13 }] }],
  "note": "Prices are in FLAM tokens. A tool with no rows falls back to the default charge."
}

check_balance answers { balance, grant, topup, held, holds }. The balance belongs to the house, never to a seat. Rates and plans: quality and cost.

How a purchase becomes yours

Worth knowing before you build anything on top of a checkout. A browser returning from payment grants nothing. The payment provider's signed delivery is the only thing that writes an entitlement, verified and deduplicated on arrival. Read the entitlement, never the redirect.

Designed and not built

The thinking is settled on all of this and none of it answers. Cost your integration on the right-hand column.

Not builtWhat to do instead
Outbound webhooks. run.started, run.frame.settled, run.settled, run.failed, purchase.granted dispatch nowhereHold the jobs socket, or poll get_develop
Streaming progress. No SSE on the MCP endpoint, no partial statestart_develop returns a jobId; get_develop reports it landing
Per-key permissions and per-key spend limitsviewer or member, and revoke
A screen for keysMint, list and revoke over HTTP
MCP resources, prompts and server-initiated samplingThe six tools
OAuth on the MCP endpointThe organization key, or a browser session
An MCP upload toolPOST /api/toolkit/assets/upload with the same key

When the webhooks land, a change to a public capability becomes one change across five places at once: the route, its runnable request, its OpenAPI description, its webhook event and its MCP tool.