Noon stores images and font files as content-addressed assets. You upload bytes once over HTTP, then point content at the returned id. This page shows the upload call and every way a piece of content can reference an uploaded asset.
Upload is HTTP, not an MCP tool
Bytes never travel through an MCP tool call. Upload with a plain HTTP request:
POST /api/v1/assets
Authorization: Bearer <token with content:write scope>
Content-Type: multipart/form-data
The multipart body needs one file part named file. A worked example:
curl -X POST https://your-site.example/api/v1/assets \
-H "Authorization: Bearer $NOON_TOKEN" \
-F "[email protected];type=image/png"
A successful upload returns 201 with this shape:
{
"id": "as_9f2c1b...",
"filename": "hero.png",
"mime": "image/png",
"size": 84213,
"sha256": "6f9e2c...",
"width": 1600,
"height": 900,
"variants": [
{ "profile": "web-v1", "width": 640, "height": 360, "mime": "image/webp", "bytes": 21044, "hash": "a1b2c3..." },
{ "profile": "web-v1", "width": 1280, "height": 720, "mime": "image/webp", "bytes": 58212, "hash": "d4e5f6..." }
],
"deduplicated": false,
"processing": { "status": "processed", "width": 1600, "height": 900, "variants": [ "..." ] }
}
deduplicated: true means the exact same bytes were already stored. Noon returns the existing asset and writes nothing new: no new row, no new file, no new processing run.
processing.status tells you what happened to the image:
| Status | Meaning |
|---|---|
processed | Resized WebP variants were generated and are ready to serve. |
original_only | The original was stored, but no variants exist yet. reason explains why: processor_unavailable (the server has no image processor installed), animated_source (an animated GIF or WebP is never flattened), or encode_failed. |
not_applicable | The upload is a font, or it was a duplicate of an existing asset. |
If processing.status is original_only, you can retry variant generation later without re-uploading:
POST /api/v1/assets/{id}/actions
Content-Type: application/json
{"action": "reprocess"}
Accepted types
Noon accepts exactly five declared MIME types: image/png, image/jpeg, image/gif, image/webp, and font/woff2. Nothing else is accepted, including SVG. SVG is refused on purpose: it is executable content, and Noon does not run a sanitizer on it.
The declared type in your multipart part must match the file's own magic bytes. A PNG declared as image/jpeg is refused, even though PNG itself is an accepted type. A mismatch, or a type outside the five, returns 415.
The per-file size cap is set by the server operator through NOON_MEDIA_MAX_BYTES. A file exactly at the cap is accepted; one byte over is refused with 413.
Finding assets you already uploaded
Assets are a normal collection in the query tool:
{
"collection": "assets",
"select": ["id", "filename", "mime", "bytes", "hash", "width", "height", "createdAt"],
"orderBy": [{ "field": "createdAt", "dir": "desc" }],
"limit": 20
}
Note the field names: the collection exposes bytes for file size and hash for the sha256 checksum. These are stable public names, not the underlying column names.
Using an uploaded asset
An asset's id is a lookup key. Here are the three places you write it into content.
In an ImageBlock, inside a page layout:
{
"type": "ImageBlock",
"props": {
"assetId": "as_9f2c1b...",
"alt": "A sunrise over the hills",
"caption": "Optional caption text"
}
}
alt is capped at 300 characters and optional; caption is optional and unbounded.
As a post's cover image, in the post's frontmatter:
{
"coverImage": { "assetId": "as_9f2c1b...", "alt": "A sunrise over the hills" }
}
Both assetId and alt are required in this shape, and alt must be 1 to 300 characters. Nothing else belongs in coverImage: no URL, no dimensions, no variant list. The renderer resolves the current image, its dimensions and its variants from the asset itself, so reprocessing an image never requires touching the post.
Inline in a post body, write the canonical path directly in the MDX text:
Here is the diagram: /assets/as_9f2c1b...
Noon recognizes this exact /assets/{id} form when it tracks which assets a post uses.
Serving assets
An uploaded original is served at:
GET /assets/{id}
A resized variant is served at:
GET /assets/{id}/variants/{hash}
Use the hash from the variants array returned on upload (or from describe_schema/query), not a hash you construct yourself. Both routes respond 404 for any failure: unknown id, unknown variant hash, or a missing file on disk. There is no separate error message that would let someone tell an unknown asset apart from a deleted one.
Both routes serve with Cache-Control: public, max-age=31536000, immutable. This is safe because the URL itself is content-addressed: editing an image produces different bytes, a different id or hash, and therefore a different URL. The URL never changes underneath a cached response.
Deleting an asset
Deletion is HTTP only, with a required conditional header:
DELETE /api/v1/assets/{id}
If-Match: "<the asset's current sha256>"
Read the current sha256 from the assets collection (field hash) before deleting. A missing If-Match header is refused with 409. A stale or wrong hash is also refused with 409.
Noon refuses to delete an asset that anything still references. This includes an ImageBlock in a published or draft page layout, a post's cover image or an /assets/{id} path in its body, a user-defined entity's data, a historical content version, a theme font, or a site setting such as the favicon or logo. If the asset is still referenced, the delete fails with a referenced_by response listing where it is used, and nothing is deleted. Remove every reference first, then delete.