{"openapi":"3.1.0","info":{"title":"mediamart API","version":"1.0.0","summary":"One flat media library, re-rendered for every platform surface.","description":"mediamart accepts an original (image, video, audio or PDF), auto-tags it, and\nrenders one derivative per platform preset. Uploads are presigned: the API never\nsees the bytes on the REST path. Authentication is a session cookie or an API key;\nevery `/v1/*` route accepts either unless it is marked otherwise.\n\nErrors use the envelope `{ \"error\": \"CODE\", \"issues\"?: unknown }`. The MCP endpoint\nuses JSON-RPC errors instead — see `/llms.txt` and `docs/api/mcp.md`.","license":{"name":"UNLICENSED"}},"servers":[{"url":"/","description":"The API itself (same origin in production)."}],"tags":[{"name":"auth","description":"Sessions, signup, password reset."},{"name":"assets","description":"Upload, finalize, list, search, fetch and delete assets."},{"name":"keys","description":"Workspace API keys for machines."},{"name":"media","description":"Platform preset registry and derivate planning."},{"name":"usage","description":"Plan limits and the current month’s meter."},{"name":"mcp","description":"Model Context Protocol server (stateless JSON-RPC 2.0)."},{"name":"docs","description":"The specification and its agent-facing companions."},{"name":"health","description":"Liveness and build information."}],"security":[],"paths":{"/health":{"get":{"tags":["health"],"operationId":"getHealth","summary":"Liveness probe","description":"Reports the preset registry size and the date it was last checked. No auth, no database.","responses":{"200":{"description":"The API is up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/openapi.json":{"get":{"tags":["docs"],"operationId":"getOpenApiDocument","summary":"This OpenAPI 3.1 document","responses":{"200":{"description":"The machine-readable contract.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/docs":{"get":{"tags":["docs"],"operationId":"getDocsPage","summary":"Human-readable API reference","description":"A self-contained HTML page. It loads Redoc from a CDN when the browser can reach it and otherwise renders a plain endpoint list linking to `/openapi.json`.","responses":{"200":{"description":"HTML documentation.","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/llms.txt":{"get":{"tags":["docs"],"operationId":"getLlmsTxt","summary":"Agent-facing Markdown guide","description":"Plain Markdown: what the product is, auth in three lines, the six endpoints that matter (with curl), the MCP endpoint and its tools, rate limits and quota errors.","responses":{"200":{"description":"Markdown.","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/v1/auth/signup":{"post":{"tags":["auth"],"operationId":"signup","summary":"Create an account and a workspace","description":"Sets the session cookie and returns the same token in the body for clients that cannot store cookies. Five signups per IP per 15 minutes.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupBody"}}}},"responses":{"201":{"description":"Account created and signed in.","headers":{"Set-Cookie":{"$ref":"#/components/headers/SetSessionCookie"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}},"400":{"description":"`invalid_request`, `EMAIL_INVALID`, `NAME_INVALID` or `PASSWORD_TOO_SHORT`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"`EMAIL_TAKEN` — the address already has an account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Per-API-key fixed window exhausted (default 120 requests/minute). `Retry-After` says when to try again.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/auth/login":{"post":{"tags":["auth"],"operationId":"login","summary":"Sign in with email and password","description":"Sets the session cookie and returns the token. Limited per email address (not per IP): 10 attempts per 15 minutes.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginBody"}}}},"responses":{"200":{"description":"Signed in.","headers":{"Set-Cookie":{"$ref":"#/components/headers/SetSessionCookie"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"`INVALID_CREDENTIALS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Per-API-key fixed window exhausted (default 120 requests/minute). `Retry-After` says when to try again.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/auth/logout":{"post":{"tags":["auth"],"operationId":"logout","summary":"Destroy the current session","description":"Deletes the session row when a credential was presented, clears the cookie, and always answers 204.","security":[{"sessionCookie":[]},{"bearerApiKey":[]},{}],"responses":{"204":{"description":"Signed out (idempotent).","headers":{"Set-Cookie":{"$ref":"#/components/headers/ClearSessionCookie"}}}}}},"/v1/auth/logout-all":{"post":{"tags":["auth"],"operationId":"logoutAll","summary":"Destroy every session for the user","description":"Needs a session (cookie, or the same session token as `Authorization: Bearer <64 hex>`); an API key cannot end user sessions.","security":[{"sessionCookie":[]}],"responses":{"204":{"description":"All sessions, including this one, are gone.","headers":{"Set-Cookie":{"$ref":"#/components/headers/ClearSessionCookie"}}},"401":{"description":"`unauthenticated` — sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/auth/me":{"get":{"tags":["auth"],"operationId":"getMe","summary":"The signed-in user","description":"Needs a session (cookie, or a session token as bearer). An API key is not a user session.","security":[{"sessionCookie":[]}],"responses":{"200":{"description":"The public user record.","content":{"application/json":{"schema":{"type":"object","required":["user"],"properties":{"user":{"$ref":"#/components/schemas/PublicUser"}}}}}},"401":{"description":"`unauthenticated`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/auth/password":{"post":{"tags":["auth"],"operationId":"changePassword","summary":"Change the signed-in user’s password","description":"Every other session for this user becomes stale; the calling session keeps working. Needs a session, not an API key.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordBody"}}}},"responses":{"204":{"description":"Password changed.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"}}},"400":{"description":"`invalid_request` or `PASSWORD_TOO_SHORT`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"`unauthenticated` or `INVALID_CREDENTIALS`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/auth/password-reset":{"post":{"tags":["auth"],"operationId":"requestPasswordReset","summary":"Email a password-reset link","description":"Always answers 202 with `{ ok: true }`, whether the address exists or not (no account-existence oracle). Five requests per address per 15 minutes.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetBody"}}}},"responses":{"202":{"description":"Accepted; a mail is sent only when the address has an account.","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","const":true}}}}}},"400":{"description":"`invalid_request`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/auth/password-reset/confirm":{"post":{"tags":["auth"],"operationId":"confirmPasswordReset","summary":"Consume a reset token and set a new password","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetConfirmBody"}}}},"responses":{"204":{"description":"Password replaced; the token is single-use."},"400":{"description":"`invalid_request`, `INVALID_TOKEN` or `PASSWORD_TOO_SHORT`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/keys":{"get":{"tags":["keys"],"operationId":"listKeys","summary":"List the workspace’s API keys","description":"Full keys are never returned; `prefix` is the first 10 characters.","security":[{"sessionCookie":[]},{"bearerApiKey":[]}],"responses":{"200":{"description":"The workspace keys, newest first.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyList"}}}},"401":{"description":"No valid session cookie and no valid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Authenticated, but the account has no workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"post":{"tags":["keys"],"operationId":"createKey","summary":"Mint an API key","description":"The full key (`mm_` + 48 hex) is returned exactly once. Only its SHA-256 digest is stored.","security":[{"sessionCookie":[]},{"bearerApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyBody"}}}},"responses":{"201":{"description":"The new key material. It cannot be recovered later.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MintedKey"}}}},"400":{"description":"A body, query or path failed its zod schema. `issues` carries the zod issue objects (or strings for `/v1/plan`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"No valid session cookie and no valid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Authenticated, but the account has no workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/keys/{id}":{"delete":{"tags":["keys"],"operationId":"revokeKey","summary":"Revoke an API key","security":[{"sessionCookie":[]},{"bearerApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"204":{"description":"Revoked.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"}}},"400":{"description":"A body, query or path failed its zod schema. `issues` carries the zod issue objects (or strings for `/v1/plan`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"No valid session cookie and no valid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Authenticated, but the account has no workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"The asset or key does not exist in the caller’s workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/assets":{"get":{"tags":["assets"],"operationId":"listAssets","summary":"List and search the library","description":"Cursor pagination over `(createdAt, id)` descending. The cursor is opaque; pass `next` back unchanged.","security":[{"sessionCookie":[]},{"bearerApiKey":[]}],"parameters":[{"name":"q","in":"query","description":"Free text matched against the title and the AI caption.","schema":{"type":"string","maxLength":200}},{"name":"tags","in":"query","description":"Comma-separated tag names; the asset must carry all of them.","schema":{"type":"string","maxLength":400}},{"name":"platform","in":"query","description":"Only assets that already have a derivative for this platform.","schema":{"type":"string","maxLength":40}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":30}},{"name":"cursor","in":"query","description":"The `next` value from the previous page.","schema":{"type":"string","maxLength":300}}],"responses":{"200":{"description":"One page of summaries plus the next cursor.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetList"}}}},"400":{"description":"A body, query or path failed its zod schema. `issues` carries the zod issue objects (or strings for `/v1/plan`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"No valid session cookie and no valid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Authenticated, but the account has no workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Object storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"post":{"tags":["assets"],"operationId":"createUpload","summary":"Create an upload ticket","description":"Creates the asset row in `uploading` and returns a presigned PUT URL.\nThe client PUTs the bytes to `uploadUrl` with the same `Content-Type`, then calls\n`POST /v1/assets/{id}/finalize`. The content type is checked against the accepted\nlist; `kind` is derived from it and never taken from the client.\n\nAccepted: `image/jpeg`, `image/png`, `image/webp`, `image/avif`, `image/gif`,\n`image/heic`, `video/mp4`, `video/quicktime`, `video/webm`, `audio/mpeg`,\n`audio/mp4`, `audio/wav`, `audio/ogg`, `application/pdf`. Maximum 200 MB.","security":[{"sessionCookie":[]},{"bearerApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUploadBody"}}}},"responses":{"201":{"description":"The asset row exists; the URL expires after `expiresIn` seconds.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadTicket"}}}},"400":{"description":"A body, query or path failed its zod schema. `issues` carries the zod issue objects (or strings for `/v1/plan`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"No valid session cookie and no valid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Authenticated, but the account has no workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Unexpected server failure. The message is never client-controlled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Object storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/assets/suggest":{"get":{"tags":["assets"],"operationId":"suggestAssets","summary":"Type-ahead suggestions","description":"Up to six matching tag names and six matching asset titles. Registered before `/v1/assets/{id}` so `suggest` is not read as an id.","security":[{"sessionCookie":[]},{"bearerApiKey":[]}],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":120}}],"responses":{"200":{"description":"Tags and titles containing the fragment.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuggestResponse"}}}},"400":{"description":"`VALIDATION` — missing or overlong `q`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"No valid session cookie and no valid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Authenticated, but the account has no workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/assets/{id}":{"get":{"tags":["assets"],"operationId":"getAsset","summary":"Fetch one asset","description":"Signed original and thumbnail URLs are minted per response and expire after 900 seconds.","security":[{"sessionCookie":[]},{"bearerApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Asset id (UUID) returned by `POST /v1/assets` or found in the list.","schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"The full asset, including every derivative.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetDto"}}}},"400":{"description":"A body, query or path failed its zod schema. `issues` carries the zod issue objects (or strings for `/v1/plan`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"No valid session cookie and no valid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Authenticated, but the account has no workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"The asset or key does not exist in the caller’s workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Object storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"tags":["assets"],"operationId":"deleteAsset","summary":"Delete an asset","description":"Removes the stored objects first, then the rows in one transaction. Quota is freed immediately.","security":[{"sessionCookie":[]},{"bearerApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Asset id (UUID) returned by `POST /v1/assets` or found in the list.","schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"204":{"description":"Deleted.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"}}},"400":{"description":"A body, query or path failed its zod schema. `issues` carries the zod issue objects (or strings for `/v1/plan`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"No valid session cookie and no valid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Authenticated, but the account has no workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"The asset or key does not exist in the caller’s workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Object storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/assets/{id}/finalize":{"post":{"tags":["assets"],"operationId":"finalizeUpload","summary":"Finalize an upload and enqueue the pipeline","description":"HEADs the object at the key returned by `POST /v1/assets`, records its real bytes and\ncontent type, moves the asset to `analyzing` and enqueues analysis/render work.\nReturns immediately; poll `GET /v1/assets/{id}` until `ready` or `failed`.","security":[{"sessionCookie":[]},{"bearerApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Asset id (UUID) returned by `POST /v1/assets` or found in the list.","schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"The asset in `analyzing`, with whatever derivatives exist already.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetDto"}}}},"400":{"description":"`VALIDATION` or `UPLOAD_MISSING` — no object at the ticket key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"No valid session cookie and no valid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Authenticated, but the account has no workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"The asset or key does not exist in the caller’s workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"`QUOTA_EXCEEDED` — this upload would exceed the plan storage quota.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Unexpected server failure. The message is never client-controlled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Object storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/plan":{"post":{"tags":["media"],"operationId":"planDerivatives","summary":"Plan crops and resizes without uploading","description":"Pure computation against the preset registry. `platforms` filters by platform key;\nomitted or empty means every preset. NOTE: an invalid body is answered with HTTP 200\nand `{ error: \"invalid_request\", issues: string[] }` — this is the existing route\nbehaviour, preserved here rather than silently changed.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanBody"}}}},"responses":{"200":{"description":"Either a plan list or the `invalid_request` envelope.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/PlanSuccess"},{"$ref":"#/components/schemas/ErrorEnvelope"}]}}}}}}},"/v1/platforms":{"get":{"tags":["media"],"operationId":"listPlatforms","summary":"The platform preset registry","description":"Every surface mediamart can render, with its source and checked date.","security":[],"responses":{"200":{"description":"The registry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformsResponse"}}}}}}},"/v1/usage":{"get":{"tags":["usage"],"operationId":"getUsage","summary":"Plan, meters and storage quota","description":"Calendar month in UTC. A `limit` of -1 means unlimited. Storage counts originals only; deleting an asset frees it immediately.","security":[{"sessionCookie":[]},{"bearerApiKey":[]}],"responses":{"200":{"description":"The workspace usage summary.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageSummary"}}}},"401":{"description":"No valid session cookie and no valid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Authenticated, but the account has no workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/mcp":{"post":{"tags":["mcp"],"operationId":"mcpPost","summary":"Send one JSON-RPC 2.0 message","description":"Stateless MCP over Streamable HTTP. Authentication is a bearer API key only\n(`Authorization: Bearer mm_…`), not the session cookie. Methods: `initialize`,\n`ping`, `tools/list`, `tools/call`; notifications are accepted with 202 and no body.\nSupported protocol versions: `2026-07-28` (current), `2025-06-18`, `2025-03-26`,\n`2024-11-05`. `GET` and `DELETE` return 405 with `Allow: POST`.","security":[{"bearerApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpRequest"}}}},"responses":{"200":{"description":"A JSON-RPC result or error. Tool failures are still HTTP 200 with `isError: true`.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"Mcp-Protocol-Version":{"$ref":"#/components/headers/McpProtocolVersion"},"Access-Control-Expose-Headers":{"description":"Exposes `Mcp-Protocol-Version` to browser clients.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpResponse"}}}},"202":{"description":"Notification accepted; no body.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"Mcp-Protocol-Version":{"$ref":"#/components/headers/McpProtocolVersion"}}},"400":{"description":"Malformed JSON (JSON-RPC `-32700`), bad envelope (`-32600`), bad params (`-32602`) or unknown method (`-32601`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpResponse"}}}},"401":{"description":"Missing key → a JSON-RPC error with code `-32001`. A present but malformed/revoked `mm_…` key is rejected earlier by the global key hook with the REST envelope `{ \"error\": \"UNAUTHENTICATED\" }`.","headers":{"WWW-Authenticate":{"$ref":"#/components/headers/WwwAuthenticate"}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/McpResponse"},{"$ref":"#/components/schemas/ErrorEnvelope"}]}}}},"429":{"description":"Per-API-key fixed window exhausted (default 120 requests/minute). `Retry-After` says when to try again.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"get":{"tags":["mcp"],"operationId":"mcpGet","summary":"No server-to-client stream","description":"The optional GET stream is not offered: 405 with `Allow: POST`.","responses":{"405":{"description":"`METHOD_NOT_ALLOWED`.","headers":{"Allow":{"$ref":"#/components/headers/Allow"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"tags":["mcp"],"operationId":"mcpDelete","summary":"No session to terminate","description":"The server is stateless, so there is nothing to DELETE: 405 with `Allow: POST`.","responses":{"405":{"description":"`METHOD_NOT_ALLOWED`.","headers":{"Allow":{"$ref":"#/components/headers/Allow"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}},"components":{"securitySchemes":{"sessionCookie":{"type":"apiKey","in":"cookie","name":"mediamart_session","description":"The web session cookie set by signup/login (opaque 64-hex token, HttpOnly, SameSite=Lax, 30 days)."},"bearerApiKey":{"type":"http","scheme":"bearer","description":"`Authorization: Bearer mm_<48 hex>`. Only the SHA-256 digest is stored server-side. The same header also accepts a raw session token (64 hex) for mobile clients; API-key auth is the machine path."}},"headers":{"XRateLimitLimit":{"description":"Requests allowed in the current per-key window (default 120/minute).","schema":{"type":"integer","minimum":0}},"XRateLimitRemaining":{"description":"Requests left in the current window.","schema":{"type":"integer","minimum":0}},"XRateLimitReset":{"description":"Unix timestamp (seconds) when the window resets.","schema":{"type":"integer"}},"RetryAfter":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"Allow":{"description":"The methods this resource supports.","schema":{"type":"string","const":"POST"}},"McpProtocolVersion":{"description":"The protocol revision the server answers with.","schema":{"type":"string","enum":["2026-07-28"]}},"WwwAuthenticate":{"description":"Bearer challenge for MCP clients.","schema":{"type":"string","const":"Bearer realm=\"mediamart\""}},"SetSessionCookie":{"description":"The session cookie: `mediamart_session=<64 hex>; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000` (`Secure` in production).","schema":{"type":"string"}},"ClearSessionCookie":{"description":"Clears `mediamart_session`.","schema":{"type":"string"}}},"schemas":{"ErrorCode":{"type":"string","description":"Every error code the API emits in the `{ error }` envelope.","enum":["UNAUTHENTICATED","FORBIDDEN","NOT_FOUND","VALIDATION","UPLOAD_MISSING","STORAGE_UNAVAILABLE","QUOTA_EXCEEDED","RATE_LIMITED","INTERNAL","METHOD_NOT_ALLOWED","invalid_request","unauthenticated","rate_limited","EMAIL_INVALID","NAME_INVALID","PASSWORD_TOO_SHORT","EMAIL_TAKEN","INVALID_CREDENTIALS","INVALID_TOKEN"]},"ErrorEnvelope":{"type":"object","description":"The one error shape for every non-MCP route.","required":["error"],"properties":{"error":{"$ref":"#/components/schemas/ErrorCode"},"issues":{"description":"zod issue objects for asset/key routes; plain strings for `/v1/plan`; absent otherwise.","oneOf":[{"type":"array","items":{"type":"object","additionalProperties":true}},{"type":"array","items":{"type":"string"}}]},"message":{"type":"string","description":"Human hint, present only on some `unauthenticated` responses."}},"additionalProperties":false},"PublicUser":{"type":"object","required":["id","email","name","workspaceId","createdAt"],"properties":{"id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":["string","null"]},"workspaceId":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"}}},"SignupBody":{"type":"object","required":["name","email","password"],"properties":{"name":{"type":"string","description":"2–100 characters; no links or HTML."},"email":{"type":"string","format":"email","maxLength":320},"password":{"type":"string","minLength":8}}},"LoginBody":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string"}}},"ChangePasswordBody":{"type":"object","required":["currentPassword","newPassword"],"properties":{"currentPassword":{"type":"string"},"newPassword":{"type":"string","minLength":8}}},"PasswordResetBody":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}},"PasswordResetConfirmBody":{"type":"object","required":["token","password"],"properties":{"token":{"type":"string","description":"The single-use token from the reset link."},"password":{"type":"string","minLength":8}}},"AuthResponse":{"type":"object","required":["user","token"],"properties":{"user":{"$ref":"#/components/schemas/PublicUser"},"token":{"type":"string","description":"The same opaque session token as the cookie, for `Authorization: Bearer <token>`."}}},"AssetAi":{"type":"object","description":"Vision-model output. Present once analysis has run; null before that.","properties":{"caption":{"type":"string"},"objects":{"type":"array","items":{"type":"string"}},"colors":{"type":"array","items":{"type":"string"}},"faces":{"type":"integer","minimum":0},"safety":{"type":"string"}},"additionalProperties":false},"Derivative":{"type":"object","description":"One rendered platform variant. `mimeType` mirrors the stored format: `image/jpeg` for still renders (the card derivative is always a JPEG — a poster frame for video, a waveform for audio), `video/mp4` for video presets and `audio/mpeg` for audio; legacy rows can carry `image/png`, `image/webp` or `image/gif`.","required":["platform","surface","width","height","bytes","url","presetVersion","mimeType"],"properties":{"platform":{"type":"string"},"surface":{"type":"string"},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1},"bytes":{"type":"integer","minimum":0},"url":{"type":"string","format":"uri","description":"Presigned GET URL."},"presetVersion":{"type":"string","description":"`<checked>:<aspect>` at render time."},"mimeType":{"type":"string","description":"IETF media type of the stored file. Still renders are `image/jpeg` (legacy rows may be `image/png`, `image/webp` or `image/gif`); video presets are `video/mp4` (or `video/webm`/`video/quicktime`); audio is `audio/mpeg` (or another audio type). An unrecognised legacy format is `application/octet-stream`."}},"additionalProperties":false},"AssetDto":{"type":"object","required":["id","title","kind","status","mimeType","bytes","width","height","aspect","tags","ai","createdAt","url","thumbUrl","derivatives"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"kind":{"type":"string","enum":["image","video","audio","doc"]},"status":{"type":"string","enum":["uploading","analyzing","ready","failed"]},"mimeType":{"type":"string"},"bytes":{"type":"integer","minimum":0},"width":{"type":["integer","null"]},"height":{"type":["integer","null"]},"aspect":{"type":["string","null"],"description":"Reduced ratio such as `3:2`; null until dimensions are measured."},"tags":{"type":"array","items":{"type":"string"}},"ai":{"oneOf":[{"$ref":"#/components/schemas/AssetAi"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time"},"url":{"type":["string","null"],"format":"uri","description":"Presigned original (GET, 900 s). Null while `uploading`."},"thumbUrl":{"type":["string","null"],"format":"uri","description":"Presigned card derivative. Null until the pipeline has produced one."},"derivatives":{"type":"array","items":{"$ref":"#/components/schemas/Derivative"}}},"additionalProperties":false},"AssetSummaryDto":{"type":"object","description":"`AssetDto` without `derivatives`, plus the platform list the grid needs. `thumbUrl` falls back to the card derivative.","required":["id","title","kind","status","mimeType","bytes","width","height","aspect","tags","ai","createdAt","url","thumbUrl","platforms"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"kind":{"type":"string","enum":["image","video","audio","doc"]},"status":{"type":"string","enum":["uploading","analyzing","ready","failed"]},"mimeType":{"type":"string"},"bytes":{"type":"integer","minimum":0},"width":{"type":["integer","null"]},"height":{"type":["integer","null"]},"aspect":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"ai":{"oneOf":[{"$ref":"#/components/schemas/AssetAi"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time"},"url":{"type":["string","null"],"format":"uri"},"thumbUrl":{"type":["string","null"],"format":"uri"},"platforms":{"type":"array","items":{"type":"string"},"description":"Platform keys with at least one derivative."}},"additionalProperties":false},"AssetList":{"type":"object","required":["assets","next"],"properties":{"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetSummaryDto"}},"next":{"type":["string","null"],"description":"Opaque cursor for the next page; null when the page is the last."}},"additionalProperties":false},"CreateUploadBody":{"type":"object","required":["name","contentType","bytes"],"properties":{"name":{"type":"string","minLength":1,"maxLength":255},"contentType":{"type":"string","minLength":1,"maxLength":120},"bytes":{"type":"integer","minimum":0,"description":"Expected size; 200 MB maximum."}}},"UploadTicket":{"type":"object","required":["assetId","uploadUrl","key","expiresIn"],"properties":{"assetId":{"type":"string"},"uploadUrl":{"type":"string","format":"uri","description":"Presigned PUT URL."},"key":{"type":"string","description":"The object key the asset row points at."},"expiresIn":{"type":"integer","description":"Seconds until `uploadUrl` expires (600)."}},"additionalProperties":false},"SuggestResponse":{"type":"object","required":["tags","titles"],"properties":{"tags":{"type":"array","maxItems":6,"items":{"type":"string"}},"titles":{"type":"array","maxItems":6,"items":{"type":"string"}}},"additionalProperties":false},"PlanBody":{"type":"object","required":["kind","width","height","bytes"],"properties":{"kind":{"type":"string","enum":["image","video"]},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1},"bytes":{"type":"integer","minimum":0},"platforms":{"type":"array","items":{"type":"string"}}}},"CropPlan":{"type":"object","required":["platform","surface","aspect","width","height","mode","crop","estimatedBytes","presetVersion"],"properties":{"platform":{"type":"string"},"surface":{"type":"string"},"aspect":{"type":"string"},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1},"mode":{"type":"string","enum":["crop","pad"],"description":"`pad` when the source is below the platform minimum; never upscaled."},"crop":{"type":"object","required":["x","y","width","height"],"properties":{"x":{"type":"integer","minimum":0},"y":{"type":"integer","minimum":0},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}},"additionalProperties":false},"estimatedBytes":{"type":"integer","minimum":0,"description":"Pixel-ratio estimate for quota gating, never billing."},"presetVersion":{"type":"string"}},"additionalProperties":false},"PlanSuccess":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CropPlan"}}},"additionalProperties":false},"PlatformSurface":{"type":"object","required":["platform","surface","aspect","formats","source","checked","confidence"],"properties":{"platform":{"type":"string"},"surface":{"type":"string"},"aspect":{"type":"string","description":"Planning target, e.g. `9:16`."},"accepted":{"type":"string","description":"What the vendor literally publishes, when it is a range or prose."},"minWidth":{"type":"integer","minimum":1},"minHeight":{"type":"integer","minimum":1},"maxWidth":{"type":"integer","minimum":1},"maxBytes":{"type":"integer","minimum":0},"formats":{"type":"array","items":{"type":"string"},"description":"Accepted container/codec names. `jpg` renders via sharp, `mp4` via ffmpeg."},"notes":{"type":"string"},"source":{"type":"string","format":"uri"},"checked":{"type":"string","format":"date"},"confidence":{"type":"string","enum":["verified","secondary"]}},"additionalProperties":false},"PlatformsResponse":{"type":"object","required":["data","count"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PlatformSurface"}},"count":{"type":"integer","minimum":0}},"additionalProperties":false},"UsageSummary":{"type":"object","required":["plan","analyses","exports","storage"],"properties":{"plan":{"type":"string","enum":["free","pro","team","scale"]},"analyses":{"$ref":"#/components/schemas/Meter"},"exports":{"$ref":"#/components/schemas/Meter"},"storage":{"type":"object","required":["usedBytes","limitBytes"],"properties":{"usedBytes":{"type":"integer","minimum":0},"limitBytes":{"type":"integer","description":"-1 means unlimited."}},"additionalProperties":false}},"additionalProperties":false},"Meter":{"type":"object","required":["used","limit"],"properties":{"used":{"type":"integer","minimum":0},"limit":{"type":"integer","description":"-1 means unlimited."}},"additionalProperties":false},"ApiKeyView":{"type":"object","required":["id","name","prefix","createdAt","lastUsedAt"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"prefix":{"type":"string","description":"First 10 characters of the key (`mm_` + 7 hex)."},"createdAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":["string","null"],"format":"date-time"}},"additionalProperties":false},"KeyList":{"type":"object","required":["keys"],"properties":{"keys":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeyView"}}},"additionalProperties":false},"CreateKeyBody":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":80}}},"MintedKey":{"type":"object","required":["id","key"],"properties":{"id":{"type":"string"},"key":{"type":"string","pattern":"^mm_[0-9a-f]{48}$","description":"Shown once; only its SHA-256 is stored."}},"additionalProperties":false},"McpRequest":{"type":"object","description":"One JSON-RPC 2.0 message. Batches are not supported. `params` is validated per method.","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","const":"2.0"},"id":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"null"}],"description":"Absent for notifications, which are answered 202 with no body."},"method":{"type":"string","description":"`initialize`, `ping`, `tools/list`, `tools/call`, or a `notifications/*` method (no id → 202)."},"params":{"type":"object","additionalProperties":true,"description":"`initialize`: `{ protocolVersion?, _meta? }`; `tools/list`: `{}`; `tools/call`: `{ name, arguments? }`."}},"additionalProperties":true},"McpTool":{"type":"object","required":["name","title","description","inputSchema"],"properties":{"name":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"inputSchema":{"type":"object","additionalProperties":true}},"additionalProperties":false},"McpContent":{"type":"object","required":["type","text"],"properties":{"type":{"type":"string","const":"text"},"text":{"type":"string","description":"JSON-stringified tool payload on success, or an `McpToolError` (`{ \"error\": { code, message } }`) when `isError` is true."}},"additionalProperties":false},"McpToolResult":{"type":"object","required":["content","isError"],"properties":{"content":{"type":"array","items":{"$ref":"#/components/schemas/McpContent"}},"isError":{"type":"boolean"}},"additionalProperties":false},"JsonRpcError":{"type":"object","required":["code","message"],"properties":{"code":{"type":"integer","description":"`-32700` parse, `-32600` invalid request, `-32601` method not found, `-32602` invalid params, `-32001` unauthorized, `-32022` unsupported protocol version."},"message":{"type":"string"},"data":{"type":"object","additionalProperties":true}},"additionalProperties":false},"McpToolError":{"type":"object","description":"The JSON object inside a failed `tools/call` text block (`isError: true`). Not the same envelope as REST errors.","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["VALIDATION","NOT_FOUND","PAYLOAD_TOO_LARGE","UPLOAD_MISSING","QUOTA_EXCEEDED","TOO_LARGE","INTERNAL"]},"message":{"type":"string"}},"additionalProperties":false}},"additionalProperties":false},"McpResponse":{"type":"object","required":["jsonrpc"],"properties":{"jsonrpc":{"type":"string","const":"2.0"},"id":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"null"}]},"result":{"type":"object","additionalProperties":true,"description":"Method-specific: initialize metadata, `{}` for ping, `{ tools }` for tools/list, or an `McpToolResult`."},"error":{"$ref":"#/components/schemas/JsonRpcError"}},"additionalProperties":false},"HealthResponse":{"type":"object","required":["status","service","presets","updated"],"properties":{"status":{"type":"string","const":"ok"},"service":{"type":"string","const":"mediamart-api"},"presets":{"type":"integer","minimum":0},"updated":{"type":["string","null"],"description":"The registry’s last checked date."}},"additionalProperties":false}}}}