{"openapi":"3.1.0","info":{"title":"Stronghold API","version":"1.0.0","description":"Stronghold is a library of creative assets: fonts, gradients, palettes, icons and shaders.\n\nThe API exists so those assets can leave the browser. Beyond browsing and searching, every\nasset can be converted to the formats other tools read — Adobe swatches, GIMP palettes and\ngradients, colour-grading LUTs, SVG sprites, Iconify collections, framework component packs,\nfont bundles, ISF shaders — and a mixed selection can be downloaded as a single style kit.\n\n## Authentication\n\nBrowsing is public. Anything tied to a user (collections, favourites) needs a caller\nidentity, and admin routes need an admin role.\n\nTwo credentials are accepted:\n\n- **Session cookie** - Alexandria SSO. The `jwt` cookie is verified locally against\n  Alexandria's JWKS; access also requires an assignment to the `stronghold` app.\n- **API key** - an Alexandria `ak_...` key for scripts, CI and MCP clients. Send it as\n  `X-API-Key` or `Authorization: Bearer ak_...`. The key must be scoped to `stronghold`.\n  Add `X-Stronghold-User: <uuid>` for user-scoped routes. A key never grants admin.\n\nA key that is present but invalid returns 401 rather than falling back to anonymous\naccess, so a broken credential surfaces instead of silently degrading.\n\n## Visibility\n\nScraped assets are global. A user upload is private to its owner, so the same id can 404 for\none caller and resolve for another.","license":{"name":"Asset licenses vary","url":"https://stronghold.manganum.com/docs#licensing"}},"servers":[{"url":"https://stronghold.manganum.com","description":"This server"}],"tags":[{"name":"Assets","description":"Browse and search the library."},{"name":"Export","description":"Convert assets into formats other tools read."},{"name":"Collections","description":"Per-user collections and favourites."},{"name":"Meta","description":"Service metadata."}],"paths":{"/health":{"get":{"tags":["Meta"],"summary":"Liveness probe","operationId":"health","security":[],"responses":{"200":{"description":"The service is up."}}}},"/api/openapi.json":{"get":{"tags":["Meta"],"summary":"This document","operationId":"openapi","security":[],"responses":{"200":{"description":"The OpenAPI description of this API."}}}},"/api/fonts":{"get":{"tags":["Assets"],"summary":"List fonts","description":"Paginated fonts. Private uploads are only visible to the user that owns them.","operationId":"listFonts","parameters":[{"name":"q","in":"query","description":"Case-insensitive substring match on the asset name.","schema":{"type":"string"}},{"name":"page","in":"query","description":"1-based page number. Values below 1 are treated as 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","description":"Items per page. Clamped to the endpoint maximum.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"category","in":"query","description":"Font category, e.g. `Sans Serif`.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","required":["items","total","page","limit","pages"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Asset"}},"total":{"type":"integer","description":"Total matching rows, ignoring pagination."},"page":{"type":"integer"},"limit":{"type":"integer"},"pages":{"type":"integer"},"filters":{"type":"object","description":"Echo of the applied sort and the seed used, when sorting randomly.","properties":{"sort":{"type":"string"},"seed":{"type":"string","nullable":true}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api/gradients":{"get":{"tags":["Assets"],"summary":"List gradients","description":"Paginated gradients. Private uploads are only visible to the user that owns them.","operationId":"listGradients","parameters":[{"name":"q","in":"query","description":"Case-insensitive substring match on the asset name.","schema":{"type":"string"}},{"name":"page","in":"query","description":"1-based page number. Values below 1 are treated as 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","description":"Items per page. Clamped to the endpoint maximum.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"type","in":"query","description":"Gradient type, e.g. `linear`, `radial`, `cosine`, `coons-mesh`.","schema":{"type":"string"}},{"name":"source","in":"query","description":"Scrape source slug.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","required":["items","total","page","limit","pages"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Asset"}},"total":{"type":"integer","description":"Total matching rows, ignoring pagination."},"page":{"type":"integer"},"limit":{"type":"integer"},"pages":{"type":"integer"},"filters":{"type":"object","description":"Echo of the applied sort and the seed used, when sorting randomly.","properties":{"sort":{"type":"string"},"seed":{"type":"string","nullable":true}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api/palettes":{"get":{"tags":["Assets"],"summary":"List palettes","description":"Paginated palettes. Private uploads are only visible to the user that owns them.","operationId":"listPalettes","parameters":[{"name":"q","in":"query","description":"Case-insensitive substring match on the asset name.","schema":{"type":"string"}},{"name":"page","in":"query","description":"1-based page number. Values below 1 are treated as 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","description":"Items per page. Clamped to the endpoint maximum.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"source","in":"query","description":"Scrape source slug.","schema":{"type":"string"}},{"name":"colors","in":"query","description":"Exact number of colours in the palette.","schema":{"type":"integer","minimum":1}},{"name":"sort","in":"query","description":"Ordering. `random` is seeded, so paging through a shuffled list never repeats or skips an item; pass the `seed` returned in `filters` to keep the same shuffle.","schema":{"type":"string","enum":["name-asc","name-desc","colors-asc","colors-desc","newest","random"],"default":"name-asc"}},{"name":"seed","in":"query","description":"Shuffle seed. Only meaningful with `sort=random`; generated when omitted.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","required":["items","total","page","limit","pages"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Asset"}},"total":{"type":"integer","description":"Total matching rows, ignoring pagination."},"page":{"type":"integer"},"limit":{"type":"integer"},"pages":{"type":"integer"},"filters":{"type":"object","description":"Echo of the applied sort and the seed used, when sorting randomly.","properties":{"sort":{"type":"string"},"seed":{"type":"string","nullable":true}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api/icons":{"get":{"tags":["Assets"],"summary":"List icons","description":"Paginated icons. Private uploads are only visible to the user that owns them.","operationId":"listIcons","parameters":[{"name":"q","in":"query","description":"Case-insensitive substring match on the asset name.","schema":{"type":"string"}},{"name":"page","in":"query","description":"1-based page number. Values below 1 are treated as 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","description":"Items per page. Clamped to the endpoint maximum.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"set","in":"query","description":"Icon set slug, e.g. `lucide`, `fa6-solid`.","schema":{"type":"string"}},{"name":"sort","in":"query","description":"Ordering. `random` is seeded, so paging through a shuffled list never repeats or skips an item; pass the `seed` returned in `filters` to keep the same shuffle.","schema":{"type":"string","enum":["name-asc","name-desc","newest","random"],"default":"name-asc"}},{"name":"seed","in":"query","description":"Shuffle seed. Only meaningful with `sort=random`; generated when omitted.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","required":["items","total","page","limit","pages"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Asset"}},"total":{"type":"integer","description":"Total matching rows, ignoring pagination."},"page":{"type":"integer"},"limit":{"type":"integer"},"pages":{"type":"integer"},"filters":{"type":"object","description":"Echo of the applied sort and the seed used, when sorting randomly.","properties":{"sort":{"type":"string"},"seed":{"type":"string","nullable":true}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api/shaders":{"get":{"tags":["Assets"],"summary":"List shaders","description":"Paginated shaders. Private uploads are only visible to the user that owns them.","operationId":"listShaders","parameters":[{"name":"q","in":"query","description":"Case-insensitive substring match on the asset name.","schema":{"type":"string"}},{"name":"page","in":"query","description":"1-based page number. Values below 1 are treated as 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","description":"Items per page. Clamped to the endpoint maximum.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"source","in":"query","description":"Scrape source slug.","schema":{"type":"string"}},{"name":"type","in":"query","description":"Shader type, e.g. `generative`, `transition`.","schema":{"type":"string"}},{"name":"language","in":"query","description":"Shader language; ISF sources are normalized to `glsl`.","schema":{"type":"string"}},{"name":"sort","in":"query","description":"Ordering. `random` is seeded, so paging through a shuffled list never repeats or skips an item; pass the `seed` returned in `filters` to keep the same shuffle.","schema":{"type":"string","enum":["name-asc","name-desc","newest","random"],"default":"name-asc"}},{"name":"seed","in":"query","description":"Shuffle seed. Only meaningful with `sort=random`; generated when omitted.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","required":["items","total","page","limit","pages"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Asset"}},"total":{"type":"integer","description":"Total matching rows, ignoring pagination."},"page":{"type":"integer"},"limit":{"type":"integer"},"pages":{"type":"integer"},"filters":{"type":"object","description":"Echo of the applied sort and the seed used, when sorting randomly.","properties":{"sort":{"type":"string"},"seed":{"type":"string","nullable":true}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api/patterns":{"get":{"tags":["Assets"],"summary":"List patterns","description":"Paginated patterns. Private uploads are only visible to the user that owns them.","operationId":"listPatterns","parameters":[{"name":"q","in":"query","description":"Case-insensitive substring match on the asset name.","schema":{"type":"string"}},{"name":"page","in":"query","description":"1-based page number. Values below 1 are treated as 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","description":"Items per page. Clamped to the endpoint maximum.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"source","in":"query","description":"Scrape source slug; every pattern is generated, so this is `generated-patterns`.","schema":{"type":"string"}},{"name":"type","in":"query","description":"Pattern family, e.g. `stripes`, `dots`, `bricks`, `basketweave`.","schema":{"type":"string"}},{"name":"tile","in":"query","description":"Exact tile size in pixels, e.g. `24`.","schema":{"type":"integer","minimum":1}},{"name":"starred","in":"query","description":"Only the caller's starred patterns. Signed out this matches nothing.","schema":{"type":"string","enum":["1"]}},{"name":"sort","in":"query","description":"Ordering. `random` is seeded, so paging through a shuffled list never repeats or skips an item; pass the `seed` returned in `filters` to keep the same shuffle.","schema":{"type":"string","enum":["name-asc","name-desc","tile-asc","tile-desc","newest","random"],"default":"name-asc"}},{"name":"seed","in":"query","description":"Shuffle seed. Only meaningful with `sort=random`; generated when omitted.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of results.","content":{"application/json":{"schema":{"type":"object","required":["items","total","page","limit","pages"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Asset"}},"total":{"type":"integer","description":"Total matching rows, ignoring pagination."},"page":{"type":"integer"},"limit":{"type":"integer"},"pages":{"type":"integer"},"filters":{"type":"object","description":"Echo of the applied sort and the seed used, when sorting randomly.","properties":{"sort":{"type":"string"},"seed":{"type":"string","nullable":true}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api/upload":{"post":{"tags":["Assets"],"summary":"Upload a private image or font","description":"Multipart. The **bytes** decide what the file is — the declared content type and the filename are hints — and an unrecognised file is refused rather than stored. Images: PNG, JPEG, GIF, WebP, SVG (sanitised before storage, since it is served back as a document the browser executes). Fonts: WOFF2, WOFF, TTF, OTF, which need a `family`. Everything uploaded is private: `is_global` stays false, `uploaded_by` is the caller, and the stored bytes are readable only by them.","operationId":"uploadAsset","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"},"name":{"type":"string","description":"Images: a label. Defaults to the filename."},"family":{"type":"string","description":"Fonts: the family name. Required."},"weight":{"type":"integer","description":"Fonts: 100–1000 in hundreds. Default 400."},"style":{"type":"string","enum":["normal","italic"],"description":"Fonts: default normal."},"license":{"type":"string","description":"Stored verbatim; the library never guesses a licence."},"tags":{"type":"string","description":"Comma-separated."}}}}}},"responses":{"201":{"description":"The created row: `{ type, id, name, path }`, plus width and height for an image."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"413":{"description":"Larger than the limit for its kind: 25 MB for an image, 8 MB for a font."},"415":{"description":"The bytes are not a supported image or font."}}}},"/api/fonts/categories":{"get":{"tags":["Assets"],"summary":"Font categories with counts","operationId":"fontCategories","responses":{"200":{"description":"Categories and how many families each holds."}}}},"/api/fonts/css":{"get":{"tags":["Assets"],"summary":"@font-face CSS for a set of families","description":"Google-hosted families return an `@import` pointing at the Google CDN; locally stored families return `@font-face` rules pointing at `/api/files/…`.","operationId":"fontCss","parameters":[{"name":"ids","in":"query","required":true,"description":"Comma-separated font ids. Invalid ids are dropped; at most 100 are used.","schema":{"type":"string"}}],"responses":{"200":{"description":"A stylesheet.","content":{"text/css":{"schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api/assets/{id}":{"get":{"tags":["Assets"],"summary":"Fetch one asset by id, across every type","operationId":"getAsset","parameters":[{"$ref":"#/components/parameters/AssetId"}],"responses":{"200":{"description":"The asset, with its type."},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/assets/{id}/download":{"get":{"tags":["Assets"],"summary":"Download a single stored font file","description":"Returns one font binary. For an entire family (every weight, `@font-face` CSS and a specimen) use `/api/export/fonts/{id}?format=zip` instead.","operationId":"downloadAsset","parameters":[{"$ref":"#/components/parameters/AssetId"}],"responses":{"200":{"description":"The file.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/files/{path}":{"get":{"tags":["Assets"],"summary":"Serve a stored file","operationId":"getFile","security":[],"parameters":[{"name":"path","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The file."},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/assets":{"get":{"tags":["Assets"],"summary":"Count every kind, or list one","description":"Without `type`, the number of assets of each kind. With it, a page of that kind — the same summaries the MCP `search_assets` tool returns, from the same definition. Only five types have a dedicated `/api/<type>` endpoint; for the other eight this is the list API. The filters a type accepts are listed in `supportedFilters` on every response, so they can be discovered rather than guessed.","operationId":"listAssets","security":[],"parameters":[{"name":"type","in":"query","description":"One asset type. `images` is accepted and answers as `textures`.","schema":{"type":"string","enum":["palettes","gradients","icons","fonts","shaders","patterns","curves","textures","luts","hdris","models","packs","audio","images"]}},{"name":"q","in":"query","description":"Substring match on the name.","schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"sort","in":"query","schema":{"type":"string","enum":["name-asc","name-desc"],"default":"name-asc"}},{"name":"source","in":"query","description":"Scrape source, e.g. `polyhaven`. One of several per-type filters; see `supportedFilters`.","schema":{"type":"string"}}],"responses":{"200":{"description":"Counts, or a page of summaries.","content":{"application/json":{"schema":{"type":"object"}}}},"302":{"description":"A page past the last one redirects to the last page."},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api/duplicates":{"get":{"tags":["Assets"],"summary":"What the library holds more than once","description":"Clusters of duplicate assets, with the evidence for each. Three signals: `same-file` (two rows pointing at one URL, or one identical file list), `same-content` (identical colours, SVG or operation stack) and `same-upload` (a normalised name *and* a matching measurement). The first two are facts and are labelled `certain`; the third is an inference labelled `likely`. Read-only: this reports, and a person decides.\n\nNothing here is derived from preview images — those are remote URLs, and a fuzzy claim about images nobody has looked at would be worse than no claim.","operationId":"listDuplicates","security":[],"parameters":[{"name":"type","in":"query","description":"Restrict the scan to one asset type. Omitted, every type is scanned.","schema":{"type":"string","enum":["palettes","gradients","icons","fonts","shaders","patterns","curves","textures","luts","hdris","models","packs","audio"]}}],"responses":{"200":{"description":"Clusters grouped by type, each with `scanned`, `truncated`, `clusters`, `redundant` and the groups themselves. `truncated` matters: \"no duplicates\" from a partial scan is not a result.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api/export/formats":{"get":{"tags":["Export"],"summary":"The export catalog","description":"Every format this server can produce. Read this rather than hard-coding format ids.","operationId":"exportFormats","security":[],"parameters":[{"name":"type","in":"query","description":"Restrict the response to one asset type.","schema":{"type":"string","enum":["palettes","gradients","icons","fonts","shaders","patterns","curves","textures","luts","hdris","models","packs","audio"]}}],"responses":{"200":{"description":"The catalog.","content":{"application/json":{"schema":{"type":"object","properties":{"maxItemsPerRequest":{"type":"integer"},"groups":{"type":"object","additionalProperties":{"type":"string"}},"types":{"type":"object","additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/ExportFormat"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api/export/{type}/{id}":{"get":{"tags":["Export"],"summary":"Export one asset","description":"### palettes\n\n| format | ext | description | batch |\n| --- | --- | --- | --- |\n| `ase` | .ase | Swatch group for the Adobe apps (Illustrator, Photoshop, InDesign, Affinity) | yes |\n| `aco` | .aco | Named swatches for the Swatches panel (Photoshop) | yes |\n| `act` | .act | Indexed-colour table, capped at 256 entries (Photoshop, GIF pipelines) | yes |\n| `gpl` | .gpl | Drop into the palettes folder (GIMP, Inkscape, Krita, MyPaint) | yes |\n| `pal` | .pal | Classic pixel-art palette format (Paint Shop Pro, Aseprite, GrafX2) | yes |\n| `soc` | .soc | Custom colour table for the Office suite (LibreOffice, OpenOffice) | yes |\n| `sketchpalette` | .sketchpalette | For the Sketch Palettes plugin (Sketch) | yes |\n| `procreate` | .swatches | Splits into palettes of 30, the Procreate limit (Procreate) | yes |\n| `css` | .css | Custom properties plus an all-stops list | yes |\n| `scss` | .scss | Variables plus a Sass map | yes |\n| `tailwind` | .js | theme.extend.colors block (Tailwind CSS 3) | yes |\n| `tailwind4` | .css | @theme block with --color-* tokens (Tailwind CSS 4) | yes |\n| `tokens` | .json | W3C design-tokens draft format (Tokens Studio, Style Dictionary) | yes |\n| `swift` | .swift | Color extension (Xcode) | yes |\n| `kotlin` | .kt | Jetpack Compose Color values (Android Studio) | yes |\n| `json` | .json | Hex, RGB, HSB and Lab for every colour | yes |\n| `txt` | .txt | One hex value per line | yes |\n| `svg` | .svg | Labelled grid, for presentations | no |\n| `png` | .png | Proportional bands, no text | no |\n\n### gradients\n\n| format | ext | description | batch |\n| --- | --- | --- | --- |\n| `css` | .css | Custom property plus a utility class | yes |\n| `tailwind` | .css | v4 @theme entry, with the v3 config in a comment (Tailwind CSS) | yes |\n| `svg` | .svg | Vector gradient, editable after import (Figma, Illustrator, Inkscape) | no |\n| `png` | .png | Rendered raster, exact at both ends | no |\n| `ggr` | .ggr | Segment-based, keeps midpoints and alpha (GIMP, Inkscape, Krita) | no |\n| `ase` | .ase | Sampled into discrete swatches (Illustrator, Photoshop) | yes |\n| `gpl` | .gpl | Sampled into 16 swatches (GIMP, Krita) | yes |\n| `cube` | .cube | 3D LUT mapping luma onto the gradient (DaVinci Resolve, Premiere, Final Cut, OBS) | no |\n| `blender` | .py | Script that builds the node, in linear colour (Blender) | no |\n| `swiftui` | .swift | Gradient stops plus the matching shape (Xcode) | no |\n| `glsl` | .glsl | vec3 ramp(float t) for shaders (Shadertoy, TouchDesigner, WebGL) | no |\n| `android` | .xml | Shape drawable with start/center/end (Android Studio) | no |\n| `json` | .json | Stops, angle, interpolation space and CSS | yes |\n\n### icons\n\n| format | ext | description | batch |\n| --- | --- | --- | --- |\n| `svg` | .svg | Single normalized icon (Figma, Illustrator, Sketch) | no |\n| `datauri` | .txt | Percent-encoded, for CSS url() | no |\n| `datauri-base64` | .txt | For tools that reject percent-encoding | no |\n| `sprite` | .svg | <symbol> sheet for <use href=\"#id\"> | yes |\n| `css` | .css | One class per icon, recoloured by currentColor | yes |\n| `css-bg` | .css | Keeps the icon’s own colours, for logos | yes |\n| `iconify` | .json | Collection JSON with shared dimensions (Iconify, unplugin-icons, Figma Iconify) | yes |\n| `react` | .zip | One .tsx per icon plus a barrel index (React, Next.js) | yes |\n| `svelte` | .zip | One .svelte per icon plus a barrel index (Svelte 5) | yes |\n| `vue` | .zip | One SFC per icon plus a barrel index (Vue 3, Nuxt) | yes |\n| `zip` | .zip | Individual files, grouped by set | yes |\n| `bundle` | .zip | Sprite, CSS, Iconify JSON and every SVG | yes |\n\n### fonts\n\n| format | ext | description | batch |\n| --- | --- | --- | --- |\n| `zip` | .zip | Every weight, @font-face CSS and a specimen | yes |\n| `css` | .css | Points at this server; @import for Google families | yes |\n| `specimen` | .html | Self-contained weights and sizes preview | no |\n| `json` | .json | Metadata, weights, axes and file list | yes |\n\n### shaders\n\n| format | ext | description | batch |\n| --- | --- | --- | --- |\n| `frag` | .frag | Stored code with a provenance header | no |\n| `standalone` | .frag | Uniform block and a real main() (glslViewer, Bonzomatic, WebGL) | no |\n| `shadertoy` | .glsl | Paste-ready; host uniforms stripped (Shadertoy) | no |\n| `isf` | .fs | Interactive Shader Format with a JSON header (VDMX, Resolume, CoGe, Millumin) | no |\n| `html` | .html | WebGL harness with no dependencies | no |\n| `json` | .json | Code plus declared uniforms | yes |\n| `zip` | .zip | Every host format plus a preview page | no |\n\n### patterns\n\n| format | ext | description | batch |\n| --- | --- | --- | --- |\n| `svg` | .svg | Real <pattern> element, so it stays a repeat after import (Figma, Illustrator, Inkscape) | no |\n| `tile` | .svg | One tile at its natural size, to build a pattern fill from (Figma, Illustrator, Sketch) | no |\n| `css` | .css | Tile inlined as a data URI, recoloured by currentColor | yes |\n| `tailwind` | .css | v4 @theme background-image entry (Tailwind CSS) | yes |\n| `sheet` | .html | Every pattern tiling, with a colour picker | yes |\n| `zip` | .zip | Tiles, tiling SVGs, CSS and a review sheet | yes |\n| `json` | .json | Tile size, markup and ready-made CSS | yes |\n\n### curves\n\n| format | ext | description | batch |\n| --- | --- | --- | --- |\n| `css` | .css | Custom property, or @keyframes for a spring | yes |\n| `tailwind` | .css | @theme --ease-* token (Tailwind CSS) | yes |\n| `js` | .ts | Standalone easing function, no dependencies | no |\n| `swiftui` | .swift | timingCurve, or an exact interpolatingSpring (Xcode) | no |\n| `flutter` | .dart | Cubic, or an exact SpringDescription (Flutter) | no |\n| `unity` | .cs | Keyframe tangents derived from the control handles (Unity) | no |\n| `glsl` | .glsl | Shader-friendly, fixed iteration count (Shadertoy, TouchDesigner, WebGL) | no |\n| `blender` | .py | Script that sets the keyframe Bezier handles (Blender) | no |\n| `aftereffects` | .jsx | Evaluates the real curve; AE influences cannot (After Effects) | no |\n| `svg` | .svg | The curve drawn in its unit box, for docs | no |\n| `sheet` | .html | Every selected curve plotted side by side | yes |\n| `zip` | .zip | Every dialect plus a plot and a readme | no |\n| `json` | .json | Definition, CSS value and sampled points | yes |\n| `csv` | .csv | 64 x,y pairs for spreadsheets and scripts | no |\n\n### textures\n\n| format | ext | description | batch |\n| --- | --- | --- | --- |\n| `blender` | .py | Builds the node graph, with correct colour spaces (Blender) | no |\n| `three` | .js | MeshStandardMaterial with the maps loaded (three.js) | no |\n| `godot` | .tres | StandardMaterial3D resource (Godot 4) | no |\n| `unity` | .cs | Editor script that sets import settings too (Unity) | no |\n| `download` | .sh | curl with resume and checksum verification | yes |\n| `zip` | .zip | Every engine, the download script and a readme | no |\n| `markdown` | .md | Maps, sizes, colour spaces and credits | no |\n| `json` | .json | Canonical map names and download URLs | yes |\n\n### luts\n\n| format | ext | description | batch |\n| --- | --- | --- | --- |\n| `cube` | .cube | 33³ by default; verified in ffmpeg, and what every NLE reads (DaVinci Resolve, Premiere, Final Cut, OBS, ffmpeg) | no |\n| `hald` | .png | 512×512 image describing a 64³ cube (ffmpeg, ImageMagick) | no |\n| `preview` | .png | Reference image, ungraded above and graded below | no |\n| `sheet` | .html | Every selected LUT applied to the same inputs | yes |\n| `zip` | .zip | Cube, 3DL, Hald, preview, recipe and a readme | no |\n| `json` | .json | The operation stack, for computing the cube yourself | yes |\n\n### hdris\n\n| format | ext | description | batch |\n| --- | --- | --- | --- |\n| `blender` | .py | World node graph with rotation and strength exposed (Blender) | no |\n| `three` | .js | Right loader, equirectangular mapping, scene.environment (three.js) | no |\n| `godot` | .tres | Environment with a panorama sky, lighting and reflections on (Godot 4) | no |\n| `unity` | .cs | Editor script: import settings, skybox material, ambient source (Unity) | no |\n| `download` | .sh | curl with resume and checksum verification | yes |\n| `zip` | .zip | Every engine, the download script and a readme | no |\n| `markdown` | .md | Resolutions, dynamic range, white balance and credits | no |\n| `json` | .json | Every resolution and format, with download URLs | yes |\n\n### models\n\n| format | ext | description | batch |\n| --- | --- | --- | --- |\n| `three` | .js | GLTFLoader, added to the scene, shadows enabled (three.js) | no |\n| `blender` | .py | Right operator per format; .blend is appended, not imported (Blender) | no |\n| `godot` | .tscn | Node3D with the imported model instanced under it (Godot 4) | no |\n| `unity` | .cs | Editor script: FBX scale, normal maps, URP material (Unity) | no |\n| `download` | .sh | Rebuilds the exact file tree the model references | yes |\n| `zip` | .zip | Every engine, the download script and a readme | no |\n| `markdown` | .md | Formats, polycount, real-world size and the file list | no |\n| `json` | .json | Every format and resolution, with relative paths | yes |\n\n### packs\n\n| format | ext | description | batch |\n| --- | --- | --- | --- |\n| `download` | .sh | Fetches, unpacks, and prints an inventory of what arrived | yes |\n| `blender` | .py | 3D kits: imports every model, laid out in a grid (Blender) | no |\n| `unity` | .cs | Fixes scale, point filtering and colour space across the pack (Unity) | no |\n| `three` | .js | 3D kits: load a piece by name, cloned and cached (three.js) | no |\n| `zip` | .zip | Everything that applies to this pack, plus a readme | no |\n| `markdown` | .md | Contents, layout and what to watch for per category | no |\n| `json` | .json | Metadata and the archive URL | yes |\n\n### audio\n\n| format | ext | description | batch |\n| --- | --- | --- | --- |\n| `credits` | .md | Title, creator, licence and source per track — what CC-BY requires | yes |\n| `attribution` | .txt | One line per track, for a credits screen or a video description | yes |\n| `download` | .sh | curl with resume, and writes the credits beside the files | yes |\n| `convert` | .sh | Mono OGG and WAV at 44.1 kHz, which is what engines want (ffmpeg, Unity, Godot) | yes |\n| `m3u` | .m3u | Streams from the source; titles are the credit lines (VLC) | yes |\n| `zip` | .zip | Credits, scripts, playlist and manifest — no audio | yes |\n| `json` | .json | Metadata, stream URLs and the composed credit line | yes |","operationId":"exportAsset","parameters":[{"$ref":"#/components/parameters/ExportType"},{"$ref":"#/components/parameters/AssetId"},{"$ref":"#/components/parameters/ExportFormat"},{"$ref":"#/components/parameters/ExportWidth"},{"$ref":"#/components/parameters/ExportHeight"},{"$ref":"#/components/parameters/ExportSteps"},{"$ref":"#/components/parameters/ExportResolution"},{"$ref":"#/components/parameters/ExportImageFormat"},{"$ref":"#/components/parameters/ExportModelFormat"}],"responses":{"200":{"description":"The exported file, with a `Content-Disposition` filename.","content":{"*/*":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/export/{type}":{"get":{"tags":["Export"],"summary":"Export several assets as one file","description":"Merges a selection into a single file where the format allows it (a `<symbol>` sprite, one stylesheet, a merged swatch list). Formats with `batch: false` reject more than one asset.","operationId":"exportBatch","parameters":[{"$ref":"#/components/parameters/ExportType"},{"$ref":"#/components/parameters/ExportFormat"},{"name":"ids","in":"query","description":"Comma-separated asset ids, at most 2000. Invalid ids are dropped. Output order follows this list.","schema":{"type":"string"}},{"name":"iconSet","in":"query","description":"Export a whole icon set instead of explicit ids. Icons only.","schema":{"type":"string"}},{"name":"source","in":"query","description":"Export everything from one scrape source.","schema":{"type":"string"}},{"name":"limit","in":"query","description":"Cap for a set/source export.","schema":{"type":"integer","minimum":1,"maximum":2000}},{"$ref":"#/components/parameters/ExportWidth"},{"$ref":"#/components/parameters/ExportHeight"},{"$ref":"#/components/parameters/ExportSteps"},{"$ref":"#/components/parameters/ExportResolution"},{"$ref":"#/components/parameters/ExportImageFormat"},{"$ref":"#/components/parameters/ExportModelFormat"}],"responses":{"200":{"description":"The exported file.","content":{"*/*":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["Export"],"summary":"Export several assets (large selections)","description":"Identical to the GET form, for selections whose id list would overflow a URL. Use this beyond roughly 40 ids.","operationId":"exportBatchPost","parameters":[{"$ref":"#/components/parameters/ExportType"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["format"],"properties":{"format":{"type":"string"},"ids":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":2000},"iconSet":{"type":"string"},"source":{"type":"string"},"limit":{"type":"integer","minimum":1},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1},"steps":{"type":"integer","minimum":2}}}}}},"responses":{"200":{"description":"The exported file.","content":{"*/*":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/export/kit":{"post":{"tags":["Export"],"summary":"Style kit for a mixed selection","description":"A zip holding every asset in its native format, plus a combined `kit.css` (font faces and colour/gradient custom properties) and an `index.html` overview. Types the kit cannot render yet are skipped.","operationId":"exportKit","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"name":{"type":"string","description":"Kit title, used in the archive and its filename."},"items":{"type":"array","maxItems":2000,"items":{"type":"object","required":["type","id"],"properties":{"type":{"type":"string","enum":["palettes","gradients","icons","fonts","shaders","patterns","curves","textures","luts","hdris","models","packs","audio"]},"id":{"type":"string","format":"uuid"}}}}}}}}},"responses":{"200":{"description":"The kit archive.","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/collections":{"get":{"tags":["Collections"],"summary":"List the caller's collections","operationId":"listCollections","responses":{"200":{"description":"Collections."},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Collections"],"summary":"Create a collection","operationId":"createCollection","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"isSmart":{"type":"boolean"},"smartFilter":{"type":"object","additionalProperties":true}}}}}},"responses":{"201":{"description":"The created collection."},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/collections/{id}":{"get":{"tags":["Collections"],"summary":"Collection with a page of its items","operationId":"getCollection","parameters":[{"$ref":"#/components/parameters/CollectionId"},{"name":"page","in":"query","description":"1-based page number. Values below 1 are treated as 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","description":"Items per page. Clamped to the endpoint maximum.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}}],"responses":{"200":{"description":"The collection and its items."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"post":{"tags":["Collections"],"summary":"Add one item, or many","description":"One item, or an `items` array of up to 200 — adding a whole page of a browse grid is one request. Every asset is checked before anything is written, and an item already present is not an error.","operationId":"addCollectionItem","parameters":[{"$ref":"#/components/parameters/CollectionId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["assetType","assetId"],"properties":{"assetType":{"type":"string","enum":["palettes","gradients","icons","fonts","shaders","patterns","curves","textures","luts","hdris","models","packs","audio","images"]},"assetId":{"type":"string","format":"uuid"}}},{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":200,"items":{"type":"object","required":["assetType","assetId"],"properties":{"assetType":{"type":"string","enum":["palettes","gradients","icons","fonts","shaders","patterns","curves","textures","luts","hdris","models","packs","audio","images"]},"assetId":{"type":"string","format":"uuid"}}}}}}]}}}},"responses":{"201":{"description":"The added item, or `{ added, alreadyPresent, total }` for the array form."},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Collections"],"summary":"Remove an item, or delete the collection","description":"With a body naming an item, that item is removed. With **no body at all**, the whole collection is deleted. A malformed body is rejected rather than falling through to deletion.","operationId":"deleteCollectionOrItem","parameters":[{"$ref":"#/components/parameters/CollectionId"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","required":["assetType","assetId"],"properties":{"assetType":{"type":"string","enum":["palettes","gradients","icons","fonts","shaders","patterns","curves","textures","luts","hdris","models","packs","audio","images"]},"assetId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"What was deleted."},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/collections/{id}/export":{"get":{"tags":["Export"],"summary":"Export a collection as a style kit","operationId":"exportCollection","parameters":[{"$ref":"#/components/parameters/CollectionId"}],"responses":{"200":{"description":"The kit archive.","content":{"application/zip":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such collection, or nothing in it can be exported yet."}}}},"/api/assets/{id}/star":{"post":{"tags":["Collections"],"summary":"Toggle a favourite","operationId":"toggleStar","parameters":[{"$ref":"#/components/parameters/AssetId"}],"responses":{"200":{"description":"The resulting starred state."},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/user/favorites":{"get":{"tags":["Collections"],"summary":"The caller's favourites","operationId":"listFavorites","responses":{"200":{"description":"Favourites."},"401":{"$ref":"#/components/responses/Unauthorized"}}}}},"components":{"securitySchemes":{"sessionCookie":{"type":"apiKey","in":"cookie","name":"jwt","description":"Alexandria SSO token. Obtained by signing in at `/login`."},"apiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"An Alexandria `ak_...` app key, scoped to the `stronghold` app. Equivalently `Authorization: Bearer ak_...`.  For a route that belongs to a user (collections, favourites), also send `X-Stronghold-User: <user uuid>` to name who the key is acting for. An API key never grants admin, so the scraper controls stay session-only."}},"parameters":{"AssetId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"CollectionId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},"ExportType":{"name":"type","in":"path","required":true,"schema":{"type":"string","enum":["palettes","gradients","icons","fonts","shaders","patterns","curves","textures","luts","hdris","models","packs","audio"]}},"ExportFormat":{"name":"format","in":"query","required":true,"description":"A format id from `/api/export/formats`.","schema":{"type":"string","enum":["aco","act","aftereffects","android","ase","attribution","blender","bundle","convert","credits","css","css-bg","csv","cube","datauri","datauri-base64","download","flutter","frag","ggr","glsl","godot","gpl","hald","html","iconify","isf","js","json","kotlin","m3u","markdown","pal","png","preview","procreate","react","scss","shadertoy","sheet","sketchpalette","soc","specimen","sprite","standalone","svelte","svg","swift","swiftui","tailwind","tailwind4","three","tile","tokens","txt","unity","vue","zip"]}},"ExportWidth":{"name":"width","in":"query","description":"Raster width in pixels. Clamped by the writer.","schema":{"type":"integer","minimum":1,"maximum":4096}},"ExportHeight":{"name":"height","in":"query","description":"Raster height in pixels. Clamped by the writer.","schema":{"type":"integer","minimum":1,"maximum":4096}},"ExportSteps":{"name":"steps","in":"query","description":"Sample count when a gradient is converted to discrete swatches.","schema":{"type":"integer","minimum":2,"maximum":1024}},"ExportResolution":{"name":"resolution","in":"query","description":"Texture resolution to wire up, as `<n>k` — textures, HDRIs and models. Defaults to the largest at or below 2k (4k for HDRIs). A resolution the asset does not have falls back to one it does rather than failing.","schema":{"type":"string","pattern":"^\\d{1,3}(k|px)$","example":"4k"}},"ExportImageFormat":{"name":"imageFormat","in":"query","description":"Preferred image format where a source offers several: `jpg`, `png`, `webp`, `exr`, `hdr`, `tif`. Textures and HDRIs.","schema":{"type":"string","enum":["jpg","jpeg","png","webp","exr","hdr","tif","tiff"]}},"ExportModelFormat":{"name":"modelFormat","in":"query","description":"Which 3D file to wire up. Models only. `three` requires a glTF and `unity` an FBX; a target that cannot read any format the model has answers 400 with the reason.","schema":{"type":"string","enum":["gltf","blend","fbx","usd"]}}},"responses":{"BadRequest":{"description":"The request was malformed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"A signed-in session is required.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"No such resource, or it is not visible to this caller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","properties":{"message":{"type":"string"}},"description":"SvelteKit's error shape."},"ExportFormat":{"type":"object","required":["id","label","extension","mime","hint","group"],"properties":{"id":{"type":"string","description":"Pass as `?format=`."},"label":{"type":"string"},"extension":{"type":"string"},"mime":{"type":"string"},"hint":{"type":"string"},"apps":{"type":"array","items":{"type":"string"},"description":"Tools that read this format."},"binary":{"type":"boolean"},"batch":{"type":"boolean","description":"Can merge several assets into one file."},"group":{"type":"string"}}},"Asset":{"type":"object","description":"Shape varies by type; these fields are common to all of them.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Fonts use `family` instead."},"source":{"type":"string","nullable":true},"sourceUrl":{"type":"string","nullable":true},"license":{"type":"string","nullable":true},"tags":{"type":"array","items":{"type":"string"},"nullable":true},"createdAt":{"type":"string","format":"date-time"}}}}},"security":[{"sessionCookie":[]},{"apiKey":[]}]}