Assets have no draft state. Uploads and metadata changes are live immediately.
Read assets#
assets_list returns paginated summaries per container, ordered by path. The other asset tools take the container and this path. An optional folder filters to a subtree.
{
"assets": [
{
"path": "hero.jpg",
"url": "/assets/images/hero.jpg",
"is_image": true,
"size": 70,
"dimensions": [1, 1],
"alt": null
}
],
"pagination": { "page": 1, "per_page": 25, "total": 1, "total_pages": 1, "next_page": null }
} assets_get adds the raw blueprint data, the shape assets_update accepts, plus mime_type, last_modified, and cp_edit_url.
Upload a file#
assets_upload takes exactly one of source_url or content_base64.
| Parameter | What it does |
|---|---|
container | Required. The asset container’s handle. |
source_url | A public http or https URL. The server downloads it. |
content_base64 | The file’s contents inline, for small files. |
filename | Required with content_base64. With source_url it defaults to the URL’s basename. |
folder | Destination folder inside the container, created on demand. |
{
"path": "blog/2026/route-map.png",
"url": "/assets/images/blog/2026/route-map.png",
"is_image": true,
"size": 70,
"dimensions": [1, 1],
"alt": null,
"result": "uploaded — live",
"cp_edit_url": "https://example.com/cp/assets/browse/images/blog/2026/route-map.png/edit"
} An upload never overwrites. A collision is an error, and the agent picks another filename.
asset 'blog/2026/route-map.png' already exists in container 'images' (id 'images::blog/2026/route-map.png') — pick another filename, or delete it first if it should be replaced Uploads use the same path as the Control Panel, so the same filename sanitizer applies, and container validation rules such as mimes:jpg,png and Statamic’s global file guards apply on top. The limit is uploads.max_size, 10240 KB by default.
- The global guards go by the filename the caller sends, and the response gives the asset’s URL. Without a
mimesrule, a caller can store what a URL returned asx.txtand read it, so set one on containers agents upload to. - A filename over 250 bytes is refused up front. So are a
.metafolder and control characters infolder. - When the disk refuses the write, the tool answers with an error and leaves nothing behind.
- When Statamic can’t read a stored file as an image, the result names the file and says what failed under
not_stored.
Set alt text afterwards#
assets_update merge-patches an asset’s metadata, meaning alt text and custom blueprint fields. The file itself is untouched. focus, the Control Panel’s focal point, passes through for lossless round-trips.
{
"container": "images",
"path": "blog/2026/route-map.png",
"data": { "alt": "Map of the coastal route" }
} How URL uploads stay off your network#
source_url is the one place this server makes outbound requests on model input. An agent can be talked into fetching anything, so the download is fail-closed. Each hop goes through these checks.
- Scheme. Only
httpandhttps. - Allowlist. If you set
uploads.source_allowlist, the host has to be on it. - DNS. The server resolves the host itself and refuses it if any address is private, loopback, link-local, carrier-grade NAT, documentation, benchmarking, multicast, or in another IANA special-purpose range that isn’t globally reachable. For IPv6 it accepts only global unicast (
2000::/3) and the NAT64 form of an address it would accept. That covers the metadata addresses169.254.169.254,100.100.100.200, andfd00:ec2::254, and the guard also refuses Azure’s wire server,168.63.129.16. One such record on a multi-record host is enough to refuse. - Pinning. The connection goes to the validated IP, whatever host curl reads from the URL, so there’s no DNS rebinding window. URL uploads need
ext-curlwithcurl_execenabled, and they’re refused without it. - Redirects. At most 3, and every hop goes through all of the above again.
- Time. The requests of one download, redirects included, share 15 seconds, though a slow DNS lookup isn’t cut off.
- Size. A
Content-Lengthoveruploads.max_sizeaborts the download. Without one, a larger body is refused when the transfer ends, which can take the full 15 seconds.
These are the real refusals, from calling the tool with a loopback address, a cloud metadata address, and a file:// URL.
source_url host '127.0.0.1' resolves to a private or reserved address — refusing to fetch source_url host '169.254.169.254' resolves to a private or reserved address — refusing to fetch source_url is not a valid absolute URL: 'file:///etc/passwd' To pin uploads to hosts you know, set the allowlist.
'uploads' => [
'max_size' => 10240,
'source_allowlist' => ['images.example.com'],
], null, the default, allows any host that passes the check above. Hosts match exactly, so wildcards don’t work. The allowlist narrows the guard and doesn’t override it, so a listed host that resolves to a refused address stays refused.
Through an HTTP proxy, a download sends CONNECT to the validated IP instead of the hostname, so a proxy that filters CONNECT by hostname or port can refuse it.
Delete an asset#
assets_delete is only registered when deletes is on, and it removes the file and its metadata. Statamic’s reference updater then strips references from entries. That runs on the queue, and it’s skipped when statamic.system.update_references is false, so an immediate re-read may still show the path in entry fields.
Paths#
A path comes from assets_list. A path containing ../ gets a tool error, and Statamic refuses it before it touches the disk.
asset path '../secret.txt' may not contain '../' — pass a path from assets_list Permissions#
| Tool | Permission |
|---|---|
assets_list, assets_get | view {container} assets |
assets_upload | upload {container} assets |
assets_update | edit {container} assets |
assets_delete | delete {container} assets |