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.
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.
| Where | What it gives you |
|---|---|
| The reference | Every operation, grouped, each with a runnable request |
| /openapi.json | The 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.
| Route | What it does |
|---|---|
POST /api/keys | Mint. Body is { name, role }; role is viewer or member and defaults to member |
GET /api/keys | Every 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
| Role | Reads | Spends tokens | Spends money | Manages keys or seats |
|---|---|---|---|---|
viewer | yes | no | no | no |
member | yes | yes | no | no |
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
| Tool | What it does | Needs |
|---|---|---|
list_capabilities | Every develop the house runs, with its token price per resolution | any key |
check_balance | Spendable tokens, the grant, top-ups, and what is on hold | any key |
list_assets | The library, newest first. folderId, limit (default 50, max 200) | any key |
start_develop | Commission one frame from asset ids | a member key |
get_develop | One develop's status, result and settled cost | any key |
list_develops | Recent develops, newest first. limit (default 20, max 100), status | any 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.
| Develop | Roles in inputs |
|---|---|
| single-image develops | image |
tryon | model, garment |
cleanup | image, mask |
packshot on your own ground | image, 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 "[email protected]" \
-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 "[email protected]" \
-F "[email protected]" \
-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 built | What to do instead |
|---|---|
Outbound webhooks. run.started, run.frame.settled, run.settled, run.failed, purchase.granted dispatch nowhere | Hold the jobs socket, or poll get_develop |
| Streaming progress. No SSE on the MCP endpoint, no partial state | start_develop returns a jobId; get_develop reports it landing |
| Per-key permissions and per-key spend limits | viewer or member, and revoke |
| A screen for keys | Mint, list and revoke over HTTP |
| MCP resources, prompts and server-initiated sampling | The six tools |
| OAuth on the MCP endpoint | The organization key, or a browser session |
| An MCP upload tool | POST /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.
Related
- What a run is: the Darkroom.
- What a frame costs: quality and cost.
- Where a develop's inputs live: collections.
Quality and cost
Tokens meter developing — 9, 13 or 19 for 1K, 2K and 4K — and the exact pixels are yours to choose. Pre-made things are bought with money, never with tokens.
API reference
Every one of the 127 routes the FLAM API serves, generated from the OpenAPI document the API itself publishes — parameters, responses and a runnable curl for each.