Files and artifacts through the API¶
On this page you upload files to use them in model requests, and download artifacts: the files
tools created. Your key needs the files capability for uploads, and both artifacts and files
for artifacts. You manage the same files with a mouse in
Files in the console.
A stored HTML file can also be shown in a browser, shared through a link, and published as a page
under a name (Pages).
Core keeps uploads and tool files in a collection for each API key. It creates the collection on the first write and uses the key id as its name. The key can access this collection even if its policy restricts other collection names. File policy and any subject access checks still apply.
Upload a file¶
curl -s -X POST "$CORE_URL/files" \
-H "Authorization: Bearer $CORE_KEY" \
-F "file=@invoice.pdf" \
-F "purpose=attachment"
Three optional fields go beside them:
collection_id-- put the file in a knowledge collection of your own instead of your key's. The collection has to be one your key reaches. In a key's own collection and in your organisation'suploadscollection the upload is a file: your key needsfiles, and the name is matched against its file policy. In every other collection the upload is a knowledge document: your key needs theknowledgecapability, or the upload answers403, and the name is not matched against the file policy. A file uploaded by a person through the management route,POST /tenants/{id}/files, goes to that organisation'suploadscollection when it names none: an administrator presents no key and so has none of their own.retention--windoworkeep.windowmeans your organisation's retention window covers the file: it is deleted once it is older than the window your administrator set (and never, if none is set).keepmeans it stays until something deletes it. Your key's own collection defaults towindow, a collection you created tokeep.folder-- put the file in a folder of its collection, a path such ascontracts/2026. Core tidies the path as it does when you move a file; empty or absent is the collection's root.
A successful upload returns 201, a Location header and file metadata:
{
"id": "src_3Wq8...",
"object": "file",
"kind": "file",
"filename": "invoice.pdf",
"purpose": "attachment",
"media_type": "application/pdf",
"bytes": 48211,
"sha256": "9f2c...",
"collection_id": "col_5Km2...",
"folder": null,
"retention": "window",
"index_status": "ready"
}
folder says where the file sits, as Core tidied the path, and is null at the collection's
root; GET /files takes folder to list one folder. Compare sha256 with your local file’s hash to verify the uploaded content.
A filename that holds a control character answers 422 on file, and nothing is stored.
Treat the id as opaque: a new upload gets an src_... id, and a file you uploaded before
2026-09-17 keeps the file_... id it was given then. Both name the same thing and work on every
call below.
Your files are searchable¶
An upload into your key's own collection is indexed, and the upload waits for it, so your very
next turn can search the file you just sent. index_status on the answer says how far that got:
index_status |
What it means |
|---|---|
ready |
the file is indexed; a search of it will find it now |
processing |
indexing is still running -- it passed the deployment's wait; ask again with GET /files/{file_id} |
queued |
indexing has not started; the worker will pick it up |
failed |
the file could not be indexed (an empty document, an unreadable one); the file itself is stored and readable |
null |
no index holds this file -- your key does not index its uploads, you put the file in a collection no index draws from, or the file is larger than any index reads (Create an index), so no index draws it in |
The upload waits up to 30 seconds for indexing to finish. That number is your deployment's own
(CORE_FILES_INGEST_WAIT_SECONDS); ask your operator if you need to know another value. A
deployment set to 0 never waits, and every upload answers queued. A file that takes longer
answers processing: indexing stops when the wait ends, and the file can stay processing for
about five minutes before indexing starts again from the beginning. Nothing is lost, and you do not
have to upload the file again.
index_error_code says why a failed file was not indexed, as one of a fixed set of codes
(Why indexing failed), and is null for every other status.
A queued file whose answer also carries parked_until is held: your key or your organisation
has reached its monthly cost cap, and indexing would spend on embeddings or on a model step of
the index's pipeline. It is indexed at that
instant, the start of the next month (UTC), or sooner once the cap is raised or lifted; you do not
have to upload it again. parked_until is null whenever nothing holds the file.
Searching it needs no tool of your own: every turn of your key is offered knowledge__uploads,
a search over that one collection -- what you uploaded and what your tools produced. Your
administrator can turn indexing off per key, in that key's files configuration
(index_uploads) on POST /tenants/{id}/keys or PATCH /tenants/{id}/keys/{key_id} -- the
console has no field for it today, so it is an API call -- and a policy that denies
knowledge__uploads removes the tool; a key without knowledge keeps its files and indexes none
of them. A file a person uploads through the management route is not waited for: it is queued
like any other knowledge source.
Who finds an upload. When your requests name the person they are for, uploads and artifacts are
decided by the rules of kind stored_file wherever they are reached: on GET /files, among a collection's sources,
and in what knowledge__uploads finds. Under the default rules a person finds her own uploads and
the shared ones, uploaded by a request that named nobody, and never another person's. Other kinds of
knowledge source follow the rules of kind knowledge_source (Build for
people).
What indexing reads from a file¶
Your key's own index has no pipeline of its own, so each file takes the default for its type: a spreadsheet, a CSV or a JSON file has its tables extracted, and every other document has its text split and embedded. Your key can choose another pipeline for that index (Templates).
A PDF is read from its text layer, the text a PDF carries for selecting and copying. Whether a
PDF has text is judged for the document as a whole, not page by page. A PDF with some scanned
pages is indexed from the pages that have text, adds nothing for the scanned pages, and is
ready. Only a PDF in which no page has text fails, with needs_ocr. To index scanned pages,
the index needs a pipeline with a parsing service (ingest/llama-parse), where your deployment
has one.
The model never sees an image inside a PDF or a Word file. The default pipeline reads text
only. With the ingest/with-images pipeline, the pictures are embedded so that a text query finds
them (Pictures found by words), but a
search hands the model a line naming the picture, [image <media type>: <path>], not the
picture itself. builtin__read_document answers text only too
(Read a whole document).
Why indexing failed¶
A failed file carries one of these codes in index_error_code; a document in a knowledge index
carries the same code as error_code in GET /knowledge/indexes/{id}/sources. The file itself
stays stored and can be downloaded whatever the code.
The default pipelines, the ones your key's own index uses, can fail with these codes:
| Code | What it means | What you do |
|---|---|---|
needs_ocr |
a PDF in which no page has text: a scan | upload a PDF with a text layer, or index it with a pipeline that has a parsing service |
needs_parse |
a Word, PowerPoint, Excel or HTML file, or a legacy .doc, in a pipeline without a step that reads it |
use a pipeline with the parse step; a .doc needs the parsing service, or save it as .docx |
decode_error |
a text file that is not valid UTF-8 | save it as UTF-8 and upload it again |
parse_error |
a file that cannot be read: a damaged PDF or Office file, a Word or PowerPoint file with no text, a CSV with duplicate or empty column names, a JSON file that is not an array of flat objects | correct the file, then upload it again |
too_large |
a table of more than 50,000 rows in a CSV, a JSON file or a worksheet; an Office file that unpacks to far more than its own size; or a file past the size an index reads, when an index draws it in through a reindex (Create an index) | split the file |
too_large_for_pipeline |
a file past 25 MiB in a pipeline that reads every file whole: any pipeline with a step beyond loading, parsing, splitting, embedding and storing, such as Structured (which CSV, JSON and Excel files take by default) or ingest/with-images |
split the file; a text, PDF or Office document is read in parts by a pipeline with only those steps (Pipeline types) |
unsupported_media_type |
a file type no step of the pipeline reads: a picture without an image step, an archive, an audio file, an unknown binary | upload a type the pipeline reads; for pictures, use ingest/with-images |
nothing_stored |
the pipeline found nothing to store: an empty document, or a text file in a pipeline that only extracts tables | check the file and the pipeline |
parse_timeout |
reading the file ran out of time, or the ingest capacity stayed busy, while reading a PDF's text layer or an Office or HTML file; or the parsing service did not finish in time | reindex later |
too_many_tables |
the document holds more tables than the pipeline extracts (50 by default) | split the document, or raise the limit in a pipeline of your own |
embedding_unavailable |
your deployment has no embedding model for your organisation | ask your operator |
model_denied |
your policy does not permit a model the pipeline calls, such as the embedding model | permit the model in the policy |
deadline_exceeded |
indexing took longer than the time one run may take | split the file, or ask your operator |
reaped_too_often |
the process indexing the file stopped five times without finishing | upload it again; if it fails again, ask your operator |
key_revoked |
the key that uploaded the file can no longer act | upload it again with a current key |
tenant_unavailable |
your organisation is suspended or being deleted | ask your operator |
pipeline_error |
anything else | reindex; if it fails again, ask your operator |
A pipeline you or your administrator built can also fail with these:
| Code | What it means | What you do |
|---|---|---|
parse_unavailable |
the pipeline uses a parsing service your deployment does not have | use a pipeline without llama_parse |
parse_failed |
the parsing service could not read the file | check the file; reindex later |
region_denied |
the parsing service runs outside the regions your key or organisation allows | name vendor:llamacloud in policy.settings.region_exceptions (Residency exceptions), or use a pipeline without llama_parse |
sheet_not_found |
the pipeline names a sheet the workbook does not have | correct the sheet name in the pipeline |
image_embedding_unavailable |
the index's embedding model reads no pictures | choose an embedding model that reads images for the index |
no_model_configured |
a model step in the pipeline names no model, and indexing has no turn to take one from | name a model in that step |
invalid_model_output |
a model step's answer was not in the form the step asks for | try another model in that step |
too_many_items |
a model step received more pieces than its limit | raise the step's limit, or split the document |
too_many_keys |
an enrichment step asks for more metadata fields than it may | ask for fewer fields |
missing_graph |
a graph step runs without a knowledge graph to write to | run the pipeline from a graph |
invalid_mapping |
a graph step maps columns or fields the document does not have | correct the mapping |
memory_write_denied |
your policy does not permit writing memories | permit memory_write in the policy |
memory_unavailable |
your deployment stores no memories | ask your operator |
pseudonymisation_failed |
personal data could not be hidden before a model step | ask your operator |
missing_source_id, missing_index_id, single_index_required |
the pipeline's store step ran without exactly one document and one index to write to | run the pipeline from one index |
component_not_implemented |
the pipeline names a step this deployment does not run | remove the step, or ask your operator |
Size limits¶
Uploads exceeding the size limit return 413. Core can reject them before reading the body if
Content-Length exceeds the limit. The deployment sets the maximum; the key’s files configuration
can lower it. Check GET /key for your key’s setting. An upload without a valid key is refused
401 before Core reads it. An upload must arrive within 300 seconds or the gateway answers 408;
at the default limit of 25 MiB that takes an upload speed of about 0.7 Mbit/s (Request time
limit).
Upload a large file in parts¶
A file larger than one upload may be, or one that has to survive a dropped connection, goes in
through an upload session: you declare the file, send it in numbered parts, and complete it.
Parts may go in any order and several at a time, and a part that failed is simply sent again.
Uploads in parts need an organisation whose files are kept in an object store; on one that keeps
them in its database the session is refused 409 and POST /files is the way in.
Open the session with the file's name, type and exact size:
curl -s -X POST "$CORE_URL/uploads" \
-H "Authorization: Bearer $CORE_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "site-survey.mp4", "media_type": "video/mp4",
"size_bytes": 734003200, "target": {"kind": "file", "purpose": "survey"}}'
The answer carries the session's id, its part_size_bytes (8 MiB unless your deployment set
otherwise) and its part_count. target takes the fields POST /files takes: purpose, and
optionally folder, collection_id, labels and retention. An optional sha256 of the whole
file makes Core check the stored bytes against it.
Cut the file into parts of exactly part_size_bytes, the last one the remainder, and send each as
the raw body:
split -b 8388608 -d -a 4 site-survey.mp4 part-
curl -s -X PUT "$CORE_URL/uploads/$SESSION/parts/1" \
-H "Authorization: Bearer $CORE_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @part-0000
Each part answers its sha256 as Core received it. A part longer than its size is 413, a
shorter one 422, and 503 with Retry-After means Core is receiving as many parts as it may at
once: wait that many seconds and send it again. After a dropped connection, GET /uploads/$SESSION
lists the parts that arrived; send the rest.
When every part has arrived, complete the session and poll it:
curl -s -X POST "$CORE_URL/uploads/$SESSION/complete" -H "Authorization: Bearer $CORE_KEY"
curl -s "$CORE_URL/uploads/$SESSION" -H "Authorization: Bearer $CORE_KEY"
Completing answers 202 and the session is completing while Core assembles and checks the file;
then it is completed and carries the file_id you use like any other file. A session that is
failed says why in error_code. DELETE /uploads/$SESSION drops an open session and its parts.
A session stays open for 24 hours by default (expires_at); after that its parts are dropped.
A session that ended can still be read for as long again, so its file_id is not lost to a late poll.
The size limit for a session is the deployment's session ceiling (5 GiB by default), lowered by
your key's own files limit. A file larger than the ordinary upload limit is stored, downloadable
and exportable, but is not made searchable.
List, download and change stored files¶
| Call | Answers |
|---|---|
GET /files |
your uploads, cursor-paged; kind and collection_id ask for other files, trashed=true for the trash |
GET /files/{file_id} |
metadata, without touching the bytes |
GET /files/{file_id}/content |
the raw bytes, with the filename in Content-Disposition |
PATCH /files/{file_id} |
the metadata after a new filename, retention, folder or collection_id |
POST /files/folders/move |
how many files moved when you rename or move a folder |
POST /files/{file_id}/restore |
the metadata of a file taken out of the trash |
DELETE /files/{file_id} |
204; the file goes to the trash, or out of it for good when it was there already |
List files¶
Without a filter, GET /files lists the uploads in your key's own collection. These query
parameters change what it lists:
kind-- which kinds of file to list. Repeat it to name several:file(an upload),artifact(a file a tool produced), andtext,recordsandconnector(knowledge documents). Core lists only the kinds you name. A kind your key lacks the capability for is left out of the list; the call itself still succeeds.collection_id-- list one other collection your key reaches instead of its own. A collection your key does not reach answers404.trashed--truelists the files in the trash instead, with the same filters.
# your uploads and the files your tools produced
curl -s "$CORE_URL/files?kind=file&kind=artifact" \
-H "Authorization: Bearer $CORE_KEY"
# every document in one knowledge collection
curl -s "$CORE_URL/files?collection_id=col_2Pf7...&kind=file&kind=text&kind=records&kind=connector" \
-H "Authorization: Bearer $CORE_KEY"
Every row says what it is in kind. A row of kind artifact also names the response_id,
call_id and tool_name that produced it.
What a file id reaches¶
/files/{file_id} takes the id of any stored file, not only of an upload. The same id works on
/artifacts/{artifact_id} when the file is an artifact, and on
/knowledge/collections/{collection_id}/sources/{source_id} for anything in that collection.
Which capability your key needs depends on what the file is, not on the route you call:
| The file | Your key needs | Also checked |
|---|---|---|
An upload: a file in a key's own collection, or in your organisation's uploads collection |
files |
the filename, against your key's file policy |
An artifact: kind artifact, in any collection |
artifacts |
a rule that permits artifact download, when you ask for the bytes |
A knowledge document: text, records, connector, and a file uploaded straight into a knowledge collection |
knowledge |
nothing more |
The /files routes themselves require files: a key that holds knowledge without files
reads its documents through the collection's source routes instead. Those routes apply the same
table: a key without artifacts finds no artifact among a collection's sources, and a key
without files finds no upload there.
A read by id is not limited to the files your own key stored. Any key of your organisation reads a file by its id when three checks all allow it:
- The collection. Your key always reaches its own collection. It reaches any other collection
-- another key's own collection, your organisation's
uploadscollection, a knowledge collection -- when your key's rules overknowledge_collectionpermit that collection's name. A key's own collection is named after the key's id (key_...). - The capability and the filename, as the table above says.
- The person. The rules over
stored_file(uploads and artifacts) orknowledge_source(every other document) decide against the person the request names inX-Core-Subjector through its subject token. Under the default rules a request for Anna reads her own uploads and artifacts and the shared ones, stored by a request that named nobody, and never another person's. Every other document reaches every person until a rule overknowledge_sourcenarrows it. A request that names nobody reads every file the first two checks allow.
Which key uploaded the file is not one of the checks. Under the default rules every key reaches every collection, so a second key of your organisation can read an upload of the first by id.
A file these checks refuse answers 404, the same answer an id that does not exist gets, on
metadata, content, update and delete alike, and a list leaves it out. A
subject token is decided the same
way, under the rules of the key it was minted from: a file in a collection those rules do not
reach answers 404, even when the file belongs to the token's own person.
To keep a key out of the other keys' collections, give it a rule over knowledge_collection that
names only the collections it should reach, in place of the default permit with the id
knowledge-collection. Its own collection stays reachable whatever the rule says:
{"id": "shared-collections", "effect": "permit", "kind": "knowledge_collection",
"match": {"names": ["uploads", "handbook-*"]}}
How to change a key's rules is in Write your first rule.
Download a file¶
curl -s "$CORE_URL/files/src_3Wq8.../content" \
-H "Authorization: Bearer $CORE_KEY" -o invoice.pdf
The answer is the stored bytes as an attachment, with Content-Length and the file's current
filename in Content-Disposition. The name is percent-encoded
(attachment; filename*=UTF-8''invoice.pdf), so decode it before you use it as a local name.
A knowledge document is downloaded the same way, here or with
GET /knowledge/collections/{collection_id}/sources/{source_id}/content: a document that was
uploaded comes back as the file that was uploaded, a text source as its text, and a records
source as a JSON array of its records. An artifact's bytes need a rule that
permits artifact download on every route (Download errors).
Rename, move or keep a file¶
curl -s -X PATCH "$CORE_URL/files/src_3Wq8..." \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"filename": "invoice-2026-041.pdf", "retention": "keep"}'
Send any of filename, retention, folder and collection_id; a field you leave out keeps
its value. The answer is the file's metadata, with updated_at set to the time of the change. A
file's content and kind are fixed.
filename-- 1 to 255 characters. For a file or an artifact -- kindfileorartifact, a file uploaded straight into a knowledge collection included -- Core keeps the last path component, soreports/q3.pdfis stored asq3.pdf, and answers422onfilenamewhen nothing is left. A source of kindtext,recordsorconnectorhas a title, not a filename, and it is stored as you wrote it. A name that holds a control character answers422onfilenamefor every kind. An upload's new name has to be one your key's file policy names; otherwise the rename answers403and the file keeps its name.retention--windowputs the file under your organisation's retention window,keepkeeps it until something deletes it.folder-- the folder to move the file into, a path such ascontracts/2026. Core tidies the path, so/contracts//2026/is the same folder, and answers422onfolderfor a.or..segment, a control character, or a path longer than 512 characters once tidied. An emptyfoldermoves the file to the collection's root. A document a connector syncs keeps the folder its origin gives it: moving one answers409, so move it at the origin instead.collection_id-- the collection to move the file into. An upload or an artifact moves only between your key's own collection and your organisation'suploadscollection, and only into one your key reaches; anything else answers422oncollection_id. A file moved out of your key's own collection is no longer searched by your key's turns. To put a file into a knowledge collection, copy it there instead.
Rename or move a folder¶
curl -s -X POST "$CORE_URL/files/folders/move" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"collection_id": "col_2Pf7...", "from": "inbox", "to": "archive/2026"}'
Every file your key reaches in from, and in the folders beneath it, moves to the same place
under to: inbox/late becomes archive/2026/late. The answer names the two folders as Core
tidied them and how many files moved. The move is all or nothing: when your key may not change
one of the files, nothing moves, and the answer is the one changing that file alone would give,
with its id. A file your key cannot see stays where it is. to equal to or inside from answers
422 on to, an empty from answers 422 on from; a collection your key does not reach
answers 404; and a folder of more than 500 files answers 422 on from; move its subfolders
first.
Delete, restore or purge a file¶
DELETE /files/{file_id} puts a file of any kind your key reaches in the trash. From that moment
it is gone from every read: it is not listed, its id answers 404, a search no longer finds it,
and a link or a view address to it stops opening. Its bytes are kept, and still count towards
what your organisation stores, until the trash is emptied: 30 days later unless your operator set
another number.
# list the trash: each file says when it was deleted and when it goes for good
curl -s "$CORE_URL/files?trashed=true" -H "Authorization: Bearer $CORE_KEY"
# put one back
curl -s -X POST "$CORE_URL/files/src_7Hq2.../restore" -H "Authorization: Bearer $CORE_KEY"
A file in the trash carries deleted_at and purge_at, which are null for a file that is not.
Its indexed text leaves every index and knowledge graph in the same request that moves it to the
trash. POST /files/{file_id}/restore puts it back where it was, in its collection and folder, and
answers its metadata; every index that draws it in indexes it again from the start, so it is
searchable once more when its index_status says ready. Indexing it again costs nothing in an
index that has not been reindexed since the file was first indexed, because that usage was already
counted; an index reindexed in the meantime, and every knowledge graph that draws the file in,
count it again. A restore is a change to the file, so your key needs what a rename
needs, and a file that is not in the trash answers 404. DELETE on a file that is already in the
trash removes it and its bytes at once; it cannot be restored after that. So a client that
retries a delete removes the file for good: before retrying, read the file with
GET /files/{file_id} and retry only on a 200; a 404 means it is already in the trash or
gone.
A file a page shows cannot be deleted while it does (A published file is kept). Three deletions skip the trash: a connector's sync removing a document its source removed, your organisation's retention window, and the erasure of a person's data. Deleting a collection removes its files for good too, those in the trash included.
A subject token renames, moves,
deletes and restores only its own person's uploads and artifacts. For a shared file, and for every
knowledge document, the call answers 403; make it from your backend with your key.
Attach files and images¶
Use input_file to attach a file reference to a user message. This provides metadata only; the
model cannot read the PDF in this example. To ask questions about its contents, supply extracted
text or index the document.
{
"model": "<a model id>",
"input": [
{
"role": "user",
"content": [
{ "type": "input_text", "text": "What is the total on this invoice?" },
{ "type": "input_file", "file_id": "src_3Wq8..." }
]
}
]
}
File and image attachments are processed differently:
- File references:
input_filesends a note containing the file id and media type. It does not parse or extract the document. - Images: synchronous calls can send
input_imagewith an uploaded image or inlinedata:image/...URL. Supported formats are PNG, JPEG, GIF and WebP, up to 5 MiB per image. Core does not fetch an image from a web address. An optionaldetail(auto,loworhigh) says how closely the model should look; it is passed to an OpenAI-family model and left out for any other. Background runs currently cannot load either form of image content. - Recordings:
input_audiocarries a recording, inline or byfile_id. Core transcribes it before the model is called and the model reads the text; see Transcribe audio. - Images in a tool result: the
outputof afunction_call_outputcan be a list ofinput_textandinput_imageparts instead of a string, when a tool you ran answers with an image. The same limits apply, and the model reads the image as part of that tool's result. In a background run it reads[image unavailable]instead.
An unavailable file id returns 404, including a file your key's policy does not name. When
continuing a conversation, Core reloads images that earlier turns attached by file_id. An inline
data: image is not stored, so a later turn reads [image unavailable] where it stood; upload the
image and send its file_id if later turns need to see it again. If an uploaded image has been
deleted or is in the trash, or your key's policy no longer names its file, the model receives [image unavailable] too
and the turn continues.
Your key's policy matches the stored filename: files it does not name are absent from GET /files
and answer 404 by id. An upload whose filename falls outside the policy answers 403 before
anything is stored. This holds for every upload that becomes a file, through POST /files and
through a collection's upload route into your key's own collection alike; a knowledge document
has no such check.
input_file also accepts an artifact id: a file a tool produced is a file like any other. So does
kind: "file" when you add a source to a knowledge collection -- but copying an artifact is
downloading it, so it is refused with 403 when no rule permits your key artifact download.
Artifacts¶
When a tool produces binary output, such as a chart or document, Core stores it as an artifact and
adds a reference to the tool result. The model receives that reference. Output over 25 MiB is
omitted and replaced with [binary output omitted: <n> bytes], where <n> is the size of the
output. An HTTP tool's answer is read no further than 25 MiB (10 MiB for JSON and text); past that
the model gets [binary output omitted: more than <n> bytes] instead, where <n> is that limit in
bytes.
Artifacts are created by tool execution. You cannot upload one directly through the API. An
artifact lands in the same collection your uploads do, and has no expiry of its own: it is
kept exactly as long as any other file there -- under your organisation's retention window, unless
you change its retention to keep.
| Call | Answers |
|---|---|
GET /artifacts?response_id=resp_... |
artifacts, optionally narrowed to one response; trashed=true lists the trash instead |
GET /artifacts/{artifact_id} |
metadata |
GET /artifacts/{artifact_id}/content |
the raw bytes, with the artifact's filename in Content-Disposition |
PATCH /artifacts/{artifact_id} |
the metadata after a new filename, retention, folder or collection_id |
POST /artifacts/{artifact_id}/restore |
the metadata of an artifact taken out of the trash |
DELETE /artifacts/{artifact_id} |
204; the artifact goes to the trash, or out of it for good when it was there already |
Every artifact carries response_id, call_id and tool_name: the response, the tool call and
the tool that produced it. It also has a filename: the name its tool gave the file, or
<tool>-<call_id> plus the extension of its media type (.png for a picture) when the tool gave
none, and a folder, null at the collection's root. A download is named after the filename, and
you can change it:
curl -s -X PATCH "$CORE_URL/artifacts/art_8Lp4..." \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"filename": "revenue-q3.png", "retention": "keep"}'
The body follows the same rules as PATCH /files/{file_id}, except that an
artifact's name is not matched against your key's file policy.
An artifact is also a stored file. GET /files?kind=artifact lists the artifacts in your key's
collection, and /files/{file_id} reads, downloads, renames, deletes and restores one by the same
id (What a file id reaches). A deleted artifact goes to the trash as a
file does (Delete, restore or purge a file).
Download errors¶
403: no rule permits your key artifact download, or a rule forbids it. This holds on every route that hands out an artifact's bytes:GET /artifacts/{artifact_id}/content,GET /files/{file_id}/contentand a collection'sGET .../sources/{source_id}/content. Ask your administrator for a rule that permits artifact download, or to remove the rule that forbids it.404: the artifact is unavailable, belongs to another organisation, is in the trash, or has already been deleted -- by you, or by your organisation's retention window closing over your key's collection. Mark the ones you need to keep withPATCH /artifacts/{artifact_id}and{"retention": "keep"}.
Pages¶
A stored HTML file can be shown in a browser instead of downloaded. An HTML file here is an upload
or an artifact whose media_type is text/html. A knowledge document cannot be shown this way,
even when it is HTML, and neither can an SVG or XHTML file.
You show an HTML file by its own id, or you publish it as a page: a name of your own over the
versions of a report, with addresses that keep working when you publish a new version (Publish a
page under a name). A page has an id of its own, pg_..., beside the ids of its
files.
There are two ways to show an HTML file or a page:
| View address | Share link | |
|---|---|---|
| For whom | yourself, or the user of your application | somebody outside your organisation, without an account |
| Valid for | a few minutes | days, until the link expires or you revoke it |
| Call | POST /files/{file_id}/view, POST /pages/{page_id}/view |
POST /files/{file_id}/links, POST /pages/{page_id}/links |
| Requires | that your key reaches the file; for an artifact, a rule that permits artifact download too | the same, and a rule that permits sharing through a link |
The file calls also exist under /artifacts/{artifact_id}, and /files/{file_id} accepts an
artifact's id (What a file id reaches).
Both addresses open the content without a key: anyone who holds the address opens the file. Treat an address as a secret. Do not write it to a log line, do not paste it into a ticket, and hand it only to people who may read the file.
Let a model write an HTML file¶
Offer the tool builtin__create_artifact to let the model store a document directly as an
artifact: an HTML file, a report, Markdown or CSV.
curl -s -X POST "$CORE_URL/responses" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{
"model": "<a model id>",
"input": "Write an overview of revenue per quarter as an HTML file.",
"tools": [{"type": "tool", "name": "builtin__create_artifact"}]
}'
Your key needs three things for this: the tooling capability, the artifacts capability, and
the bare name create_artifact under system_tools in its tooling configuration. In the
console that is the Write a document checkbox on the key's page
(Keys).
The model passes two arguments, filename and content, and nothing else. The filename's
extension decides the media type; the model does not choose it:
| Extension | media_type |
|---|---|
.html, .htm |
text/html |
.md |
text/markdown |
.txt |
text/plain |
.csv |
text/csv |
.json |
application/json |
.xml |
application/xml |
.svg |
image/svg+xml |
Only a file with .html or .htm can be opened in a browser, shared and published as a page. A filename with another
extension, or with none, is refused, and the model is given the list of allowed extensions. The
content may not be larger than your deployment's limit, 2 MiB by default; above it nothing is
stored.
When it works, the model reads Saved <filename> (<n> bytes) as an artifact. The response body
does not name the artifact, so look it up with GET /artifacts?response_id=resp_...
(Artifacts).
An HTML file must be self-contained. One file, with its styles, scripts and images included in it, and no requests to anywhere else: whoever shows the file later blocks them (What an opened file can do). The tool's description already tells the model this rule. When your own instructions ask for an HTML file, do not ask for a font, a chart library or an image from a web address.
Open an HTML file¶
Request a view address. The call has no body:
curl -s -X POST "$CORE_URL/artifacts/art_8Lp4.../view" \
-H "Authorization: Bearer $CORE_KEY"
{
"url": "https://<your Core address>/api/v1/views/vt_...",
"expires_at": "2026-10-06T12:05:00Z"
}
url opens the file in a browser until expires_at. Your deployment decides how long that is,
and it is never more than fifteen minutes. Until then the address works as often as it is loaded,
so a reload is fine. A view address cannot be withdrawn, which is why it is short-lived. Request a
new one when the old one has expired.
An address stops working earlier when the key that requested it is deleted or expires, when the file is deleted, or when your organisation is suspended or being deleted.
Show the file in one of these two ways:
- In a tab or window of its own. That always works.
- In a frame in your application:
<iframe sandbox="allow-scripts" src="...">. A browser shows the file in a frame only on a site your operator has allowed for that. On any other site the frame stays empty; ask your operator to add your application's address.
Each time the address is opened, Core writes artifact.viewed or file.viewed to the
audit trail. Requesting the address does not write that event, and a
view costs no usage.
When your request names a person, it reaches that person's files and the shared ones, as any other call on the file does.
What an opened file can do¶
An HTML file is content a model wrote or somebody uploaded, and nobody has checked it. Core therefore serves it confined, whatever your own frame allows. This holds for a view address and for a share link.
| What the content does | Result |
|---|---|
| Runs its own scripts and styles, written in the file | works |
Shows images, fonts and media carried in the file as data: |
works |
Makes a request to anywhere else: fetch, an external script or stylesheet, an image or font from a web address, another frame |
blocked |
Reads or writes cookies or localStorage |
blocked; the content has no storage |
| Reads the site it is embedded in, or calls Core with your sign-in | blocked; the content shares an origin with neither |
| Submits a form, opens a popup or starts a download | blocked |
| In a frame: sends the tab of the site it is embedded in somewhere else | blocked |
The confinement does not stop the content from leaving for another address, by a link or by script:
- In a frame, the content can send its own frame to another address. The frame then shows something from elsewhere in the place where the stored file was. Make it visible in your application that the frame shows stored content.
- In a tab of its own, which is how a share link always opens and how a view address opens when you show it that way, the content can send that tab to another address.
What leaves with such a navigation is whatever the content put in the address it goes to, which is content the viewer could already read. The view address or share link itself is not sent along.
Share an HTML file¶
A share link opens one HTML file to somebody without an account, for days, until the link expires or you revoke it. A link on a file keeps showing that file; make the link on a page when it should follow new versions.
Sharing is off by default. It is permitted by a rule of kind share_link in the policy, and
no policy document holds such a rule unless somebody adds it:
- Your organisation's policy document must permit it. An organisation that has no document of its own shares nothing, whatever a key's rules permit.
- Your key shares only when its own document permits it too.
The rule takes no names, labels or conditions. Beside the rules a document already holds, it is
one more entry in rules, shown here with the setting for the longest life:
{
"rules": [
{ "id": "share-link", "effect": "permit", "kind": "share_link" }
],
"settings": { "share_link_max_days": 14 }
}
Do not send this fragment on its own: a document holding only this rule permits nothing else. A
policy document is always sent whole, so read it first, add the rule and send it back complete:
GET and PUT /organisation/policy for the organisation, GET and PATCH /keys/{key_id} with
policy for a key. In the console your administrator adds the rule in the policy editor, as a
Permit rule about Share stored HTML files and pages through a link
(Settings, Keys).
curl -s -X POST "$CORE_URL/files/src_3Wq8.../links" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"expires_in_days": 7}'
The answer is 201 with a Location header:
{
"id": "lnk_6Tn2...",
"object": "share_link",
"url": "https://<your Core address>/api/v1/views/sl_...",
"file_id": "src_3Wq8...",
"page_id": null,
"expires_at": "2026-10-13T09:30:00Z",
"expired": false,
"closed": false,
"created_by": "key_4Hd9...",
"created_at": "2026-10-06T09:30:00Z"
}
expires_in_days is a whole number from 1 to 365. The longest life is the policy
setting share_link_max_days: the shortest limit your organisation's document and your key's
name, and 30 days when neither names one. Asking for more answers 422. Leave the field or the whole
body out and the link gets the longest life that is allowed.
| Call | Answers |
|---|---|
POST /files/{file_id}/links |
201 and the new link |
GET /files/{file_id}/links |
this file's links, oldest first, cursor-paginated |
GET /files/{file_id}/links/{link_id} |
one link |
DELETE /files/{file_id}/links/{link_id} |
204; the address stops opening at once |
The same four calls exist under /artifacts/{artifact_id}/links. Sharing an artifact is
downloading it: when no rule permits your key artifact download, the answer is 403.
A share link opens the file at the top of a tab. No site can show it in a frame. The content is confined as it is behind a view address (What an opened file can do).
Core does not store the address and composes it again on every call. Two calls therefore answer a
differently spelled url for the same link. Each of those addresses opens the same link, and they
all close together.
url is null in three cases. The link is still readable, with its id and expires_at, and
you can revoke it:
- Your key could not make this link itself now: the rules do not permit it sharing through a link, or do not permit it artifact download. The address opens the content without a key, so a caller who may not take the file out is not given the address.
- The call is made with a subject token and the file is a shared one, not that person's own. Such a token may not share a shared file, so it does not read the address either.
- The link is closed (
closed: true): the key that made it was deleted or has expired, see below.
A subject token shares only its own person's
files and revokes links only on those. For a shared file the answer is 403; make the call from
your backend with your key.
What closes a link¶
A closed address answers 404, without saying why.
| What happened | What the list shows | Does the link open again? |
|---|---|---|
You revoked the link with DELETE |
the link is gone | no |
| The page the link was made on was deleted | the link went with the page | no |
| Its life ran out | expired: true, until you revoke it |
no |
| The file is in the trash | the file's links answer 404 |
yes, once the file is restored, if the link has not expired |
| The file was purged from the trash, or its person's data was erased | the link went with the file | no |
| Your organisation's retention window deleted the file | the link went with the file | no; set retention to keep on a file you share, or publish it as a page |
The organisation's policy stopped permitting share_link: the permit rule was removed, a forbid rule was added, or the document was removed |
the link is still listed | yes, once the organisation's policy permits it again, if the link has not expired |
| The organisation is not active: suspended or being deleted | the link is still listed | yes, once the organisation is active again |
| The key that made the link was deleted | closed: true and url: null, until you revoke it |
no |
| The key that made the link has expired | closed: true and url: null, until you revoke it |
only if the key's expiry is moved later or lifted, and the link has not expired |
A link a person made in the console belongs to no key, so it does not close with a key.
Each time a share link is opened, Core writes a usage row of kind
shared_bytes with the size of the file, and file.link_viewed or
artifact.link_viewed to the audit trail. An address that no longer opens writes nothing.
Whoever holds the address can open it as often as they like; when your usage shows a link being
abused, revoke it.
Publish a page under a name¶
A report is refined, and each refinement is a new stored file with a new id. An address made on a file keeps showing that one file. A page is a name over the published versions of a report, with a pointer at the version it shows now. Publish a new version and every view address and every share link of the page shows it, at the address people already have.
The page calls need files. Publishing an artifact needs artifacts too.
curl -s -X POST "$CORE_URL/pages" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "q3-report", "file_id": "art_8Lp4..."}'
The answer is 201 with a Location header:
{
"id": "pg_2Vx7...",
"object": "page",
"name": "q3-report",
"current_version": 1,
"file_id": "art_8Lp4...",
"created_by": "key_4Hd9...",
"created_at": "2026-10-06T09:30:00Z",
"updated_at": "2026-10-06T09:30:00Z"
}
name is 1 to 64 lower-case letters, digits and hyphens, and is unique in your organisation; a
name that is taken answers 409. file_id is the HTML file that becomes version 1; a file that
is not HTML answers 409. In the answer, file_id is always the file of the version the page
shows now.
| Call | Answers |
|---|---|
POST /pages |
201 and the new page, at version 1 |
POST /pages/{page_id}/versions |
201 and the page, showing the new version |
GET /pages |
the pages your key reaches, cursor-paginated |
GET /pages/{page_id} |
one page |
GET /pages/{page_id}/versions |
the page's versions, oldest first |
PATCH /pages/{page_id} |
the page after a roll back, a rename, or both |
DELETE /pages/{page_id} |
204 |
POST /pages/{page_id}/view |
a view address for the page |
/pages/{page_id}/links, /pages/{page_id}/links/{link_id} |
share links on the page |
Your key reaches a page exactly when it reaches the file of the page's current version. A page it
does not reach is left out of GET /pages and answers 404 by id.
Publish the next version¶
curl -s -X POST "$CORE_URL/pages/pg_2Vx7.../versions" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"file_id": "art_5Rk1..."}'
The answer is the page with current_version one higher and file_id the new file. From that
moment every address of the page shows the new version; nobody who holds an address has to be
sent a new one.
When the page has a share link that still opens, a new version is published to the outside as
well. The caller therefore has to be allowed to share now, by the rules of Share an HTML
file; otherwise the answer is 403 and the page keeps showing the version it
showed. The same holds for a roll back or forward with PATCH.
A new version must be the same kind of file as the page's other versions: a page is all uploads
or all artifacts. When your requests name a person, it must also be a file of the same person as
the page. Anything else answers 409.
List versions and roll back¶
curl -s "$CORE_URL/pages/pg_2Vx7.../versions" \
-H "Authorization: Bearer $CORE_KEY"
Each version carries its number, counted from 1, the file_id it shows, and who published it
and when (created_by, created_at). A number is never used twice: a version published after a
roll back takes the next unused number.
Point the page at another of its versions with current_version, an earlier one to roll back or
a later one to roll forward:
curl -s -X PATCH "$CORE_URL/pages/pg_2Vx7..." \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"current_version": 1}'
A number the page does not have answers 422, and so does a version whose file your key does not
reach.
The same call renames the page with name; send one field or both. A name that is taken answers
409. Neither a roll back nor a rename changes the page's id, its view addresses or its share
links.
Open and share a page¶
# a view address, valid for minutes
curl -s -X POST "$CORE_URL/pages/pg_2Vx7.../view" \
-H "Authorization: Bearer $CORE_KEY"
# a share link, valid for days
curl -s -X POST "$CORE_URL/pages/pg_2Vx7.../links" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"expires_in_days": 30}'
Both answer what the file calls answer, and every rule of Open an HTML file and
Share an HTML file holds: the rule over artifact download, the rule over sharing through a link, the longest life, a null url, what the opened file can do, and what is recorded and metered.
The difference is what the address shows: whichever version is current at the moment the
address is opened. A share link you sent last week shows the version you published today.
A link on a page carries the page in page_id, and its file_id is the file of the version the
page shows now. A link on a file has page_id: null. A page's links are listed, read and revoked
under /pages/{page_id}/links only, not under the file the page happens to show.
In the audit trail a view of a page is recorded as a view of the file it showed, with page_id
beside the file's id.
A published file is kept¶
Publishing a file sets its retention to keep, so your organisation's retention window does not
delete a file a page shows. This holds for version 1 and for every later version.
While a file is a version of a page, three calls answer 409, and the message names the page:
- deleting the file, on
/files,/artifactsor a collection's sources; - setting its
retentionback towindow; - deleting a collection that holds it.
Delete the page first.
Delete a page¶
curl -s -X DELETE "$CORE_URL/pages/pg_2Vx7..." \
-H "Authorization: Bearer $CORE_KEY"
204. The name, the versions and the page's share links are gone, and every address of the page
stops opening at once. The files stay, and stay on keep. Delete them with DELETE
/files/{file_id}, or set their retention back to window, when you do not need them.
A subject token publishes, rolls back, renames and deletes only a page of its own person.
Errors for HTML files and pages¶
| Status | When | What to do |
|---|---|---|
403 |
On POST .../view and POST .../links, for a file or a page: the file, or the page's current version, is an artifact and no rule permits your key artifact download. GET .../links still answers 200, with url: null. |
Ask your administrator for a rule that permits artifact download. |
403 |
On POST .../links: your organisation's policy document or your key's holds no rule that permits share_link, or holds one that forbids it, or your organisation has no policy document. |
Have a permit rule of kind share_link added to the document that lacks it. Where a forbid rule matches, have that rule removed: forbid always wins, so adding a permit changes nothing. |
403 |
On POST /pages/{page_id}/versions and on PATCH /pages/{page_id} with current_version: the page has a share link that still opens, and the rules do not permit the caller sharing through a link now. |
Have a permit rule of kind share_link added, or the forbid rule removed, or revoke the page's links first. |
403 |
With a subject token: creating or revoking a link on a shared file, or publishing a version on, changing or deleting a page that is not that person's. Requesting a view address for a shared file is allowed. | Make the call from your backend with your key. |
404 |
Your key does not reach the file or the page, or the link does not belong to this file or page. On a view address or a share link itself: any reason it does not open, such as expired, revoked or deleted. | Check the id. For a view address, request a new one. |
409 |
The file is not an HTML file: its media_type is not text/html, or it is a knowledge document. |
Download the file with GET .../content. |
409 |
On POST /pages and PATCH /pages/{page_id}: the name is taken. |
Choose another name. |
409 |
On POST /pages/{page_id}/versions: the file is another kind than the page's other versions, or is for another person than the page. |
Publish it as a page of its own. |
409 |
Deleting a file, setting its retention to window, or deleting its collection: the file is a version of the page the message names. |
Delete the page first. |
422 |
expires_in_days is not a whole number from 1 to 365, or is longer than the policy allows. |
Ask for a shorter life, or leave the field out. |
422 |
name is not 1 to 64 lower-case letters, digits and hyphens, or current_version is a version the page does not have or whose file your key does not reach. |
Read the versions with GET /pages/{page_id}/versions. |
503 |
Your deployment is not set up to show HTML files. | Ask your operator. |