Skip to content

Powered by Grav + Helios

Media

Media

Endpoints for managing media files attached to pages and site-level media, their .meta.yaml metadata, the manual order of site media folders, and files saved by blueprint file fields. Reading needs api.media.read and changing anything needs api.media.write. Page media also follows the page's o...

Endpoints for managing media files attached to pages and site-level media, their .meta.yaml metadata, the manual order of site media folders, and files saved by blueprint file fields. Reading needs api.media.read and changing anything needs api.media.write. Page media also follows the page's own access rules, so a page that denies you read or update access returns 403. Thumbnails are public.

List Page Media

GET /pages/{route}/media
List all media files for a page, with optional metadata filtering and sorting.

Parameters

Name Type Description
route required string The page route
filter optional string Metadata filter as `field:operator:value` (or `field:value`). Repeatable via `filter[]=`. Operators: == != > >= < <= in contains.
sort optional string Metadata (or intrinsic filename/size/modified) field to sort by
order optional string Sort direction: asc (default) or desc
JSON
{"data": [{"filename": "photo.jpg", "url": "/user/pages/01.blog/photo.jpg", "type": "image/jpeg", "size": 245000, "alt": "Sunset over the bay", "dimensions": {"width": 1920, "height": 1080}, "thumbnail_url": "/api/v1/thumbnails/3f2a9c1e4b5d6a7f8e9d0c1b2a394857.jpg", "modified": "2026-09-01T10:15:00+00:00"}]}

Response Codes

200 Success
401 Unauthorized
403 Missing `api.media.read` permission, or the page's own rules deny read access
404 Page not found
422 Invalid filter/sort parameter

Filtering and sorting by metadata

When your media carry metadata via .meta.yaml sidecars, you can filter and sort the listing by those values instead of pulling the whole set. This mirrors the Twig collection query methods but is bound to the metadata schema configured under Plugins → API → Media Metadata (plugins.api.media_metadata.fields).

Filter

Each filter clause is field:operator:value. Repeat it with filter[]= to apply several (they are ANDed):

TXT
GET /pages/blog/media?filter=rating:>=:3&filter[]=tags:contains:sunset

Available operators are ==, !=, >, >=, <, <=, in, and contains. You can also omit the operator (filter=copyright:Jane Doe), in which case it is inferred from the field type — contains for a tags field, == otherwise. For the in operator, give a comma-separated set: filter=rating:in:4,5.

Sort

TXT
GET /pages/blog/media?sort=rating&order=desc

sort accepts any configured metadata field plus the intrinsic filename, size, and modified keys. order is asc (default) or desc; media missing the sorted field always sort last. (dir is accepted as an alias for order.)

Rules and limits

  • Schema-bound. Only fields defined in media_metadata.fields are filterable and sortable; unknown fields are ignored. A tags field accepts only in/contains.
  • Validation. An unknown operator, a malformed clause, an invalid sort field, or more than 10 filter clauses returns 422. Filtering rides the existing api.media.read permission and exposes no metadata values the caller could not already read.
  • Cap. At most 10 filter clauses per request.

Upload Page Media

POST /pages/{route}/media
Upload file(s) to a page via multipart form data.

Parameters

Name Type Description
route required string The page route
file required file File to upload (multipart/form-data). Any field name works, including nested array names such as `files[]`, and several files may be sent in one request.
random_name optional boolean Replace the file name with a random one, keeping the extension (default: false)
avoid_overwriting optional boolean Add a timestamp prefix instead of replacing an existing file with the same name (default: false)
accept optional string Comma-separated allowlist of extensions (`.pdf`, `*.jpg`) or MIME patterns (`image/*`)
filesize optional number Per-field maximum size in MB. Can only tighten the 64 MB cap.
JSON
{"data": [{"filename": "photo.jpg", "url": "/user/pages/01.blog/photo.jpg", "type": "image/jpeg", "size": 245000, "dimensions": {"width": 1920, "height": 1080}, "thumbnail_url": "/api/v1/thumbnails/3f2a9c1e4b5d6a7f8e9d0c1b2a394857.jpg", "modified": "2026-09-01T10:15:00+00:00"}]}

Response Codes

201 File uploaded
401 Unauthorized
403 Missing `api.media.write` permission, or the page's own rules deny update access
404 Page not found
422 Validation error (no file, invalid file name or type, or file too large)

Send the file as a multipart/form-data request rather than JSON. The Content-Type header should be set to multipart/form-data.

The response lists all of the page's media after the upload, not only the new files, and the Location header points at the page's media collection. Each file is capped at 64 MB. A file must have an extension, and no dot-separated part of its name may be on security.uploads_dangerous_extensions (so shell.php.jpg is refused). SVG content is sanitized when it arrives.

The optional random_name, avoid_overwriting, accept and filesize form fields apply a blueprint file field's upload settings on top of those checks.

Delete Page Media

DELETE /pages/{route}/media/{filename}
Delete a media file from a page.

Parameters

Name Type Description
route required string The page route
filename required string The media filename to delete

Response Codes

204 File deleted
401 Unauthorized
403 Missing `api.media.write` permission, or the page's own rules deny update access
404 Page or file not found
422 Invalid filename (leading period or `..`)

Removes the file and its .meta.yaml sidecar, if present. Retina variants of the file ([email protected], [email protected], and so on) and their sidecars are removed too, so an image stored only as a retina variant can still be deleted by its base name.

List Site Media

GET /media
List site-level media files and folders with pagination, subfolder browsing, search, type filtering, and metadata filtering and sorting.

Parameters

Name Type Description
page optional integer Page number for pagination (default: 1)
per_page optional integer Number of results per page (default: 20, max: 1000, both configurable)
path optional string Subfolder path relative to the media root directory
search optional string Case-insensitive recursive filename search across all subfolders. Cannot be combined with `filter` or `sort`.
type optional string Filter by media type: image, video, audio, or document (anything that is not image, video or audio)
filter optional string Metadata filter as `field:operator:value` (or `field:value`). Repeatable via `filter[]=`, up to 10 clauses. Same rules as page media.
sort optional string Metadata (or intrinsic filename/size/modified) field to sort by. Overrides the folder's manual order.
order optional string Sort direction: asc (default) or desc
JSON
{"data": [{"filename": "hero.jpg", "path": "blog", "url": "/user/media/blog/hero.jpg", "type": "image/jpeg", "size": 184320, "dimensions": {"width": 1600, "height": 900}, "thumbnail_url": "/api/v1/thumbnails/3f2a9c1e4b5d6a7f8e9d0c1b2a394857.jpg", "modified": "2026-09-01T10:15:00+00:00"}], "meta": {"pagination": {"page": 1, "per_page": 20, "total": 1, "total_pages": 1}, "path": "blog", "folders": [{"name": "2026", "path": "blog/2026", "children_count": 0, "file_count": 4}], "ordered": false}, "links": {"self": "/api/v1/media?page=1&per_page=20"}}

Response Codes

200 Success
401 Unauthorized
403 Missing `api.media.read` permission
422 Invalid path, `search` combined with `filter` or `sort`, or an invalid filter/sort parameter

Returns media files and folders from the user/media directory. Supports browsing subfolders via the path parameter, recursive search via search, filtering by file type via type, and filtering and sorting by .meta.yaml metadata via filter, sort and order (see List Page Media for the filter syntax). The response includes a folders array in the meta object listing immediate subdirectories at the current path, and meta.ordered says whether the folder has a saved manual order.

Files follow the folder's saved manual order (see Set Media Order) when one exists, otherwise natural filename order. .meta.yaml sidecars and the media_order.yaml order file are never listed, in a folder listing or in search results.

A folder that does not exist returns an empty list, with meta.pagination echoing the requested page and per_page and no meta.ordered. In search mode, meta.path is empty, meta.folders is empty, meta.search holds the search term, and meta.ordered is absent.

Upload Site Media

POST /media
Upload files to the site-level media directory.

Parameters

Name Type Description
path optional string Query parameter: subfolder path to upload into (created automatically if it does not exist)
file required file File to upload (multipart/form-data). Any field name works, including nested array names such as `files[]`, and several files may be sent in one request.
random_name optional boolean Replace the file name with a random one, keeping the extension (default: false)
avoid_overwriting optional boolean Add a timestamp prefix instead of replacing an existing file with the same name (default: false)
accept optional string Comma-separated allowlist of extensions (`.pdf`, `*.jpg`) or MIME patterns (`image/*`)
filesize optional number Per-field maximum size in MB. Can only tighten the 64 MB cap.
JSON
{"data": [{"filename": "hero.jpg", "path": "blog", "url": "/user/media/blog/hero.jpg", "type": "image/jpeg", "size": 184320, "dimensions": {"width": 1600, "height": 900}, "thumbnail_url": "/api/v1/thumbnails/3f2a9c1e4b5d6a7f8e9d0c1b2a394857.jpg", "modified": "2026-09-01T10:15:00+00:00"}]}

Response Codes

201 Files uploaded successfully
401 Unauthorized
403 Missing `api.media.write` permission
422 Validation error (no file, invalid path, file name or type, or file too large)

Upload one or more files to the user/media directory. Use the path query parameter to upload into a subfolder. The request body should use multipart/form-data with a file field. Maximum upload size per file is 64 MB.

The response lists only the files saved by this request. A file must have an extension, and no dot-separated part of its name may be on security.uploads_dangerous_extensions. SVG content is sanitized when it arrives. Without avoid_overwriting, a file with the same name is replaced. A 422 stops the request at that file, and files saved before it are kept.

Delete Site Media

DELETE /media/{filename}
Delete a site-level media file.

Parameters

Name Type Description
filename required string The media filename to delete. May include a subfolder path (e.g. blog/hero.jpg).

Response Codes

204 File deleted
401 Unauthorized
403 Missing `api.media.write` permission
404 File not found
422 Invalid filename (a path segment is empty, starts with a period, or is `..`)

Remove a file and its .meta.yaml sidecar (if present) from the user/media directory, and drop it from the folder's manual order. The filename parameter supports subfolder paths.

Create Media Folder

POST /media/folders
Create a new folder in the site-level media directory.
JSON
{
  "path": "blog/2026"
}
JSON
{"data": {"name": "2026", "path": "blog/2026", "children_count": 0, "file_count": 0}}

Response Codes

201 Folder created
401 Unauthorized
403 Missing `api.media.write` permission
422 Validation error or folder already exists

Create a new folder in the user/media directory. Parent directories are created automatically. The request body must include a path property with the relative folder path to create. No path segment may start with a period or be ... The Location header points at the new folder's listing (/media?path=...).

Delete Media Folder

DELETE /media/folders/{path}
Delete an empty folder from the site-level media directory.

Parameters

Name Type Description
path required string Folder path relative to the media root

Response Codes

204 Folder deleted
401 Unauthorized
403 Missing `api.media.write` permission
404 Folder not found
422 Folder is not empty, or the path is invalid

Remove an empty folder from the user/media directory. The folder must contain no files or subdirectories, and that includes hidden files, .meta.yaml sidecars and a media_order.yaml order file.

Rename Site Media

POST /media/rename
Rename or move a media file within the site-level media directory.
JSON
{
  "from": "blog/hero.jpg",
  "to": "blog/banner.jpg"
}
JSON
{"data": {"filename": "banner.jpg", "path": "blog", "url": "/user/media/blog/banner.jpg", "type": "image/jpeg", "size": 184320, "dimensions": {"width": 1600, "height": 900}, "thumbnail_url": "/api/v1/thumbnails/3f2a9c1e4b5d6a7f8e9d0c1b2a394857.jpg", "modified": "2026-09-01T10:15:00+00:00"}}

Response Codes

200 File renamed
401 Unauthorized
403 Missing `api.media.write` permission
404 Source file not found
422 Validation error or destination already exists

Rename a file or move it to a different folder within user/media. Both from and to are relative paths. The destination directory is created automatically if needed. Any .meta.yaml sidecar file is also moved, and the folders' manual orders are kept in step. The response is the file at its new location.

The new name is cleaned before it is used: whitespace becomes a dash, characters other than letters, digits, ., _ and - are dropped, and the source file's extension is always kept (any extension given in to is replaced). So {"from": "hero.jpg", "to": "My Banner.png"} produces My-Banner.jpg. If the cleaned name is the same as the source, nothing changes and the file is returned as it is. A cleaned name with no letters or digits left returns 422.

Rename Media Folder

POST /media/folders/rename
Rename a folder within the site-level media directory.
JSON
{
  "from": "blog/old-name",
  "to": "blog/new-name"
}
JSON
{"data": {"name": "new-name", "path": "blog/new-name", "children_count": 2, "file_count": 14}}

Response Codes

200 Folder renamed
401 Unauthorized
403 Missing `api.media.write` permission
404 Source folder not found
422 Validation error or destination already exists

Rename a folder within the user/media directory. All contents of the folder are preserved. Both from and to are relative paths from the media root.

The response carries the renamed folder's real children_count (subfolders) and file_count (media files, not counting sidecars or the order file), the same counts the folder listing reports, so a client can update the entry without fetching the listing again.

Serve Thumbnail

GET /thumbnails/{file}
Serve a cached image thumbnail generated by the API's thumbnail service. This endpoint is public and needs no authentication. Thumbnail filenames are hashes produced when media is serialized — Admin2 uses the URLs embedded in media responses (`thumbnail_url`, or a user's `avatar_url`) rather than requesting thumbnails by path. Responses ship with `Cache-Control: public, max-age=31536000, immutable` so they are safe to cache for a year.

Parameters

Name Type Description
file required string Thumbnail filename (basename only).

Response Codes

200 Thumbnail served.
404 Thumbnail not found in cache.

Thumbnails are stored in cache://api/thumbnails/. They are generated when an image is serialized in a media response, never by this endpoint, so a thumbnail that is not cached yet (for example after a cache clear) returns 404 until the media listing that contains it is fetched again.

Get Page Media Metadata

GET /pages/{route}/media/{filename}/meta
Read the editable metadata of a page media file, stored in its `.meta.yaml` sidecar.

Parameters

Name Type Description
route required string The page route
filename required string The media filename
JSON
{"data": {"filename": "photo.jpg", "has_meta": true, "fields": [{"key": "alt", "label": "Alt Text", "type": "text", "value": "Sunset over the bay"}, {"key": "title", "label": "Title", "type": "text", "value": ""}, {"key": "caption", "label": "Caption", "type": "textarea", "value": ""}, {"key": "description", "label": "Description", "type": "textarea", "value": ""}, {"key": "tags", "label": "Tags", "type": "tags", "value": ["beach", "summer"]}], "extra": {"width": 1920, "height": 1080}}}

Response Codes

200 Success
401 Unauthorized
403 Missing `api.media.read` permission, or the page's own rules deny read access
404 Page or file not found
422 Invalid filename (leading period or `..`)

Returns one entry in fields for each editable field configured under Plugins → API → Media Metadata (plugins.api.media_metadata.fields), with its current value. When nothing is configured, the defaults are alt, title, caption, description and tags. A text or textarea field's value is a string (empty when unset), and a tags field's value is a list of strings.

Any other keys stored in the sidecar (EXIF data, dimensions, upload info) are returned as read-only data in extra. has_meta says whether a sidecar file exists at all.

Update Page Media Metadata

PATCH /pages/{route}/media/{filename}/meta
Save the editable metadata of a page media file.

Parameters

Name Type Description
route required string The page route
filename required string The media filename
JSON
{
  "fields": {
    "alt": "Sunset over the bay",
    "tags": ["beach", "summer"]
  }
}
JSON
{"data": {"filename": "photo.jpg", "has_meta": true, "fields": [{"key": "alt", "label": "Alt Text", "type": "text", "value": "Sunset over the bay"}, {"key": "title", "label": "Title", "type": "text", "value": ""}, {"key": "caption", "label": "Caption", "type": "textarea", "value": ""}, {"key": "description", "label": "Description", "type": "textarea", "value": ""}, {"key": "tags", "label": "Tags", "type": "tags", "value": ["beach", "summer"]}], "extra": {"width": 1920, "height": 1080}}}

Response Codes

200 Metadata saved
401 Unauthorized
403 Missing `api.media.write` permission, or the page's own rules deny update access
404 Page or file not found
422 Invalid filename, or a value that is not plain text

Send the values in a fields object, or as a bare map of field keys to values. Only configured fields present in the request are changed, and an empty value (an empty string, or an empty list for a tags field) removes that field. Keys that aren't configured are ignored, and every other key already in the sidecar is kept. The response is the updated metadata, in the same format as Get Page Media Metadata.

Values render into <img> attributes on the site, so they are cleaned on save: HTML tags and control characters are stripped, single-line fields have their whitespace collapsed, and each value is cut to plugins.api.media_metadata.max_length characters (2000 by default). A tags value may be a list of strings or one comma-separated string; duplicates and blanks are dropped and at most 50 tags are kept. Reserved keys that Grav manages itself (width, height, mime, size, filesize, modified, upload, type) can never be written here.

If every key is removed, the sidecar file is deleted.

Delete Page Media Metadata

DELETE /pages/{route}/media/{filename}/meta
Clear the editable metadata of a page media file.

Parameters

Name Type Description
route required string The page route
filename required string The media filename

Response Codes

204 Metadata cleared
401 Unauthorized
403 Missing `api.media.write` permission, or the page's own rules deny update access
404 Page or file not found
422 Invalid filename (leading period or `..`)

Removes every configured metadata field from the file's .meta.yaml sidecar. Other keys, such as EXIF data or dimensions, are kept, and the sidecar file is deleted only when nothing else is left in it. The media file itself is not touched.

Serve Raw Site Media

GET /media/raw/{path}
Stream the bytes of a site-level media file that the web server will not serve directly.

Parameters

Name Type Description
path required string File path relative to the media root. May contain `/` (e.g. photos/beach.jpg).

Response Codes

200 The file bytes, with the detected MIME type as `Content-Type`
401 Unauthorized
403 Missing `api.media.read` permission
404 File not found, or not a servable media type
422 Invalid path (a segment starts with a period or is `..`)

Media listings normally give each file a direct static URL. When the site media folder sits somewhere the web server refuses to serve, such as a multi-site user/env/<host>/media folder (Grav's shipped .htaccess and nginx configs deny user/env, user/config and user/accounts), the listing's url points here instead.

Only images, video, audio, fonts and PDFs are served. Any other file type returns 404, so this route can't be used to read source or configuration files. The response is sent inline with Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox and X-Content-Type-Options: nosniff, so an uploaded SVG can't run script when it is opened directly, and with Cache-Control: private, max-age=600.

Because a browser <img> element can't send an auth header, this route accepts the site's own session cookie, and also a JWT passed as a ?token= query parameter.

Get Site Media Metadata

GET /media/meta
Read the editable metadata of a site-level media file, stored in its `.meta.yaml` sidecar.

Parameters

Name Type Description
path required string Query parameter: file path relative to the media root (e.g. blog/hero.jpg)
JSON
{"data": {"filename": "hero.jpg", "has_meta": true, "fields": [{"key": "alt", "label": "Alt Text", "type": "text", "value": "Sunset over the bay"}, {"key": "title", "label": "Title", "type": "text", "value": ""}, {"key": "caption", "label": "Caption", "type": "textarea", "value": ""}, {"key": "description", "label": "Description", "type": "textarea", "value": ""}, {"key": "tags", "label": "Tags", "type": "tags", "value": ["beach", "summer"]}], "extra": {"width": 1920, "height": 1080}}}

Response Codes

200 Success
401 Unauthorized
403 Missing `api.media.read` permission
404 File not found
422 Missing or invalid `path`

The file is addressed by the path query parameter rather than in the URL, because site media paths contain slashes. The response is the same as for Get Page Media Metadata: one entry per configured field in fields, other sidecar keys in extra, and has_meta saying whether a sidecar exists. filename is the file's base name.

Update Site Media Metadata

PATCH /media/meta
Save the editable metadata of a site-level media file.

Parameters

Name Type Description
path required string Query parameter: file path relative to the media root (e.g. blog/hero.jpg)
JSON
{
  "fields": {
    "alt": "Sunset over the bay",
    "tags": ["beach", "summer"]
  }
}
JSON
{"data": {"filename": "hero.jpg", "has_meta": true, "fields": [{"key": "alt", "label": "Alt Text", "type": "text", "value": "Sunset over the bay"}, {"key": "title", "label": "Title", "type": "text", "value": ""}, {"key": "caption", "label": "Caption", "type": "textarea", "value": ""}, {"key": "description", "label": "Description", "type": "textarea", "value": ""}, {"key": "tags", "label": "Tags", "type": "tags", "value": ["beach", "summer"]}], "extra": {"width": 1920, "height": 1080}}}

Response Codes

200 Metadata saved
401 Unauthorized
403 Missing `api.media.write` permission
404 File not found
422 Missing or invalid `path`, or a value that is not plain text

Works the same way as Update Page Media Metadata: only configured fields present in the request change, an empty value removes a field, other sidecar keys are kept, and values are cleaned on save. The response is the updated metadata. To apply the same values to many files in one request, use Batch Update Site Media Metadata.

Delete Site Media Metadata

DELETE /media/meta
Clear the editable metadata of a site-level media file.

Parameters

Name Type Description
path required string Query parameter: file path relative to the media root (e.g. blog/hero.jpg)

Response Codes

204 Metadata cleared
401 Unauthorized
403 Missing `api.media.write` permission
404 File not found
422 Missing or invalid `path`

Removes every configured metadata field from the file's .meta.yaml sidecar. Other keys are kept, and the sidecar file is deleted only when nothing else is left in it. The media file itself is not touched.

Batch Update Site Media Metadata

POST /media/batch/meta
Apply the same metadata fields to several site-level media files at once.
JSON
{
  "files": ["photos/beach.jpg", "photos/dunes.jpg"],
  "fields": {
    "tags": ["beach", "summer"]
  }
}
JSON
{"data": {"results": [{"path": "photos/beach.jpg", "status": "success"}, {"path": "photos/missing.jpg", "status": "error", "message": "Media file not found."}], "total": 2, "successful": 1, "failed": 1}}

Response Codes

200 Per-file results (check each `status`)
401 Unauthorized
403 Missing `api.media.write` permission
422 Missing `files` or `fields`, `files` empty or not a list, `fields` not an object, or too many files

files lists paths relative to the media root, and fields maps metadata field keys to values. Only the configured fields present in fields are written to each file, so a batch that sets just tags leaves each file's own alt and title alone. An empty value (an empty string, or an empty list for a tags field) removes that field. Values are cleaned the same way as in Update Site Media Metadata.

Each file is handled on its own: a bad path or a missing file is reported in results with status: error and a message, and the rest of the batch still runs. The number of files is capped by plugins.api.batch.max_items (50 by default).

Set Media Order

POST /media/order
Save a manual ordering for the files in a site-level media folder.
JSON
{
  "path": "photos",
  "order": ["dunes.jpg", "beach.jpg", "sunset.jpg"]
}

Response Codes

204 Order saved
401 Unauthorized
403 Missing `api.media.write` permission
404 Folder not found
422 Missing `order` array, or invalid `path`

path is the folder relative to the media root; leave it out for the root folder. order lists filenames in the order you want. Entries are reduced to base filenames, and blanks are dropped. An empty order removes the saved order.

The order is stored in a media_order.yaml file inside the folder, and List Site Media applies it when listing that folder. Files that aren't in the saved order (new uploads, for example) follow the ordered ones. The order file itself never appears in listings or search results, and it is kept up to date when a file is deleted, renamed or moved.

Blueprint File Upload

POST /blueprint-upload
Upload a file for a blueprint `type: file` field (a theme logo, a plugin asset, an avatar), saved to the folder the field's `destination` names.

Parameters

Name Type Description
file required file File to upload (multipart/form-data). Any field name works, and several files may be sent in one request.
destination required string The blueprint field's `destination`: a Grav stream (`theme://images/logo`, `user://media`), a `self@:subpath` token resolved against `scope`, or a plain path relative to `user/`
scope optional string Owner of the blueprint, used to resolve `self@:` destinations: `plugins/<slug>`, `themes/<slug>`, `pages/<route>` or `users/<username>`
field optional string Full name of the blueprint field the upload belongs to (`custom_css`, `header.stylesheet`). With `scope`, the server looks up the field's `allow_extensions` in the blueprint itself.
random_name optional boolean Replace the file name with a random one, keeping the extension (default: false)
avoid_overwriting optional boolean Add a timestamp prefix instead of replacing an existing file with the same name (default: false)
accept optional string Comma-separated allowlist of extensions (`.pdf`, `*.jpg`) or MIME patterns (`image/*`)
filesize optional number Per-field maximum size in MB. Can only tighten the 64 MB cap.
JSON
{"data": [{"name": "logo.png", "path": "user/themes/quark/images/logo/logo.png", "size": 20480, "type": "image/png", "url": "https://example.com/user/themes/quark/images/logo/logo.png"}]}

Response Codes

201 Files saved
401 Unauthorized
403 Missing `api.media.write` permission, a target in `user/config/` or `user/env/*/config/`, or a `users/<username>` scope for another account without `api.users.write`
422 Missing `destination`, no file, an unsafe path, or a refused file name, type or size

This mirrors classic admin's taskFilesUpload and is what Admin2 uses for blueprint file fields that set a destination. Send it as multipart/form-data. Absolute paths and .. segments in destination are rejected.

The response lists each saved file. path is the logical path under user/, built from destination and scope rather than the resolved disk path, so it still works through symlinked theme or plugin folders. Pass it to Delete Blueprint File to remove the file.

File rules:

  • Each file is capped at 64 MB, and the field's own filesize can only lower that.
  • No dot-separated part of the name may be on security.uploads_dangerous_extensions, so shell.php.jpg is refused.
  • Config and data files (yaml, yml, json, twig, env, neon, lock) are refused wherever they would land. Page content (md, markdown) and stylesheets (css, scss, sass, less) are refused too, unless the field named in field allows them with allow_extensions in the blueprint that owns scope and the upload goes to that field's own destination.
  • Uploads into user/accounts/ may only be images (jpg, jpeg, png, gif, webp, svg, avif, bmp, ico), since Grav reads that folder as its account store.
  • Uploads that resolve into user/config/ or user/env/*/config/ are refused with 403.
  • SVG files are sanitized after saving.

A self@: destination with a users/<username> scope for your own account needs only api.media.write. For another account it also needs api.users.write (or super user).

Delete Blueprint File

DELETE /blueprint-upload
Delete a file saved by a blueprint file field, plus its `.meta.yaml` sidecar if present.

Parameters

Name Type Description
path required string Logical path of the file, relative to the user folder (`user/` prefix optional)
scope optional string Owner of the field's blueprint: `plugins/<slug>`, `themes/<slug>`, `pages/<route>` or `users/<username>`. Only needed with `field`.
field optional string Full name of the blueprint field the file belongs to. With `scope`, lets a field's `allow_extensions` cover deleting its own files.
JSON
{
  "path": "user/themes/quark/images/logo/logo.png"
}

Response Codes

204 File deleted, or it was already gone
401 Unauthorized
403 Missing `api.media.write` permission, a target in `user/config/` or `user/env/*/config/`, a non-image file under `user/accounts/`, or another account's file without `api.users.write`
422 Missing `path`, traversal or null bytes in it, a refused file type, or a target that is not a regular file

path is the logical path the upload returned, relative to the user folder; the user/ prefix is optional. .. and null bytes are rejected.

The same extension rules as Blueprint File Upload apply, so config files can't be deleted through this endpoint, and neither can page content (.md, .markdown) or stylesheets (.css, .scss, .sass, .less) unless field and scope name a field whose blueprint allows them with allow_extensions and the file sits in that field's own folder. Under user/accounts/ only image files can be deleted, and without api.users.write (or super user) only your own avatar, meaning a file your account's avatar field points at.

The call is idempotent: a file that no longer exists returns 204.