Large sites
View as markdownInline, a save carries at most 25 MB a file, 50 MB in all and 2,000 files.
Beyond that, upload the big files first and name each in the save as
{ "path": "video.mp4", "upload": "<uploadId>" } — in files, or as a change
in changes — and a save with uploaded files may total 5 GB (4 GB a file).
Material has its own ceilings (see library). Three ways to upload; pick by
where the bytes are.
Bytes in your context, or no shell — upload over this connection
The connection you already hold is the only authorization: nothing is minted and no secret exists at any point.
upload_file{ op: "start", filename, size, sha256 }→{ upload, maxChunkBytes }. Declaresha256— the server then refuses corrupted bytes atcompleteinstead of hosting them.upload_file{ op: "part", upload, n, data }— base64 parts numbered from 1, in any order; send a part again to overwrite it (that is how to resume after a lost call). At most 8 MB decoded each (2–6 MB is the sweet spot), at most 1,000 parts.upload_file{ op: "complete", upload }— verifies every part, the declared size and the declared digest, and answers the upload's id and the server-computedsha256.- Save, naming the upload as
{ "path": "…", "upload": "<upload>" }. An upload is used by one save.
An upload not completed expires after 24 hours, and must happen in the
workspace the save lands in (workspace, a handle, as on every other tool).
Bytes on the person's own device — offer a place to drop them
upload_file { op: "offer", for: "save" | "library" } uploads nothing
itself. Where the host shows interfaces, it opens a hand-over: the person picks
or drops files, the surface uploads each with the three steps above over this
same connection, and then tells you each file's name and upload id — save them
as { "path": "…", "upload": "<upload>" }, or, for the library, as one piece
of material each with save_artifact { kind, name, files: [{ path, upload }] }.
The bytes never pass through the conversation. The answer carries the limits:
4 GB a file for a save, each material kind's own ceiling for the library.
Where the host shows none, nothing opens: a file on the person's machine then
goes through an upload address (below).
Files on disk and a shell at hand — an upload address
Chunking a big tree through tool calls is slow; a shell POSTs it in one go:
create_credential { kind: "upload", artifact: "<its address, identifier or key>" }→ an upload address, a capability URL scoped to that one artifact — a key nobody in the workspace holds yet makes a new artifact with that key on its first save — expiring in minutes (minutes, default 30) and good for one save unless you ask for more (saves, at most 50). The URL is the whole credential: nothing to store, and losing it risks at most its saves to one artifact for a few minutes.- Build the same JSON
save_artifacttakes, lessartifact(the address carries it), reading the files from disk, and POST it to the answer'surl:curl -X POST "$UPLOAD_URL" -H "Content-Type: application/json" -d @save.json—/api/v1/artifacts/<artifact>/versions?session=…for an artifact that exists,/api/v1/artifacts?session=…for a new one, whose body carriesnameand the address'skey(the key may be left out; it is the address's).changeson abasework from a shell too, so a few changed files of a big site need not travel again. - The same ceilings and the same answer as the tool, left-out paths included.
- Bigger single files (video and the like, up to 4 GB each): stage them with the resumable upload routes, the address's
?session=…on every call —POST /api/v1/uploads{ "filename", "size" }→{ uploadId, partSize }(16 MB parts),PUT /api/v1/uploads/<uploadId>/parts/<n>per part (put a part again to resume),POST /api/v1/uploads/<uploadId>/complete— then name them in the save as{ "path": "video.mp4", "upload": "<uploadId>" }. The answer'suploadsUrlis that staging address.
A save that is refused spends nothing, so fix it and send it again. revoke_credential ends an address early. Many pieces of material at once go to the library's own upload address ({ kind: "upload", library: true, batches }) and POST /api/v1/library — see library.
If the upload address is denied
Some agent harnesses refuse credential-minting tools — even bounded ones. Do not stall — degrade in order:
- Shell not essential?
upload_file(above) mints nothing — there is no credential call for a harness to refuse. Any size, straight over this connection; slower per byte, but it always works. - Fits inline? At most 50 MB in all and 25 MB a file saves fine through
save_artifact(base64 for binaries). Trim what the served site does not need — source maps, raw exports, unoptimized originals — before giving up on inline.
A save from a shell needs nothing of the person's: the upload address is its whole credential, and uploading over this connection needs none.
Standing credentials (CI, cron)
For automation that saves on its own schedule, create_credential { kind: "credential" } makes a long-lived credential. The secret never appears in the conversation: the answer carries a sign-in-gated link that reveals it once to its owner — have the person open it and store the secret where the automation runs. It then saves with POST /api/v1/artifacts/<artifact>/versions and a bearer header. By default a credential is publish-only — saving, uploading, adding material, and listing what exists by name and state; pass scopes when making it to add read (reads, including saved file content and drafts) and manage (settings, state, deletion, access, share links, comments — the whole management surface, mirrored at /api/v1; reference at https://23artifacts.com/docs/api). A publish-only credential — standing or an upload address — makes new artifacts with any access and preview, but on an existing one those two must match what is stored (or be left out, which keeps them); changing them is manage, and the save is refused with a 403 that says so.
get_workspace lists them and revoke_credential ends one — upload addresses and credentials alike.