Skip to content

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's uploads collection the upload is a file: your key needs files, and the name is matched against its file policy. In every other collection the upload is a knowledge document: your key needs the knowledge capability, or the upload answers 403, 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's uploads collection when it names none: an administrator presents no key and so has none of their own.
  • retention -- window or keep. window means 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). keep means it stays until something deletes it. Your key's own collection defaults to window, a collection you created to keep.
  • folder -- put the file in a folder of its collection, a path such as contracts/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), and text, records and connector (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 answers 404.
  • trashed -- true lists 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:

  1. The collection. Your key always reaches its own collection. It reaches any other collection -- another key's own collection, your organisation's uploads collection, a knowledge collection -- when your key's rules over knowledge_collection permit that collection's name. A key's own collection is named after the key's id (key_...).
  2. The capability and the filename, as the table above says.
  3. The person. The rules over stored_file (uploads and artifacts) or knowledge_source (every other document) decide against the person the request names in X-Core-Subject or 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 over knowledge_source narrows 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 -- kind file or artifact, a file uploaded straight into a knowledge collection included -- Core keeps the last path component, so reports/q3.pdf is stored as q3.pdf, and answers 422 on filename when nothing is left. A source of kind text, records or connector has a title, not a filename, and it is stored as you wrote it. A name that holds a control character answers 422 on filename for every kind. An upload's new name has to be one your key's file policy names; otherwise the rename answers 403 and the file keeps its name.
  • retention -- window puts the file under your organisation's retention window, keep keeps it until something deletes it.
  • folder -- the folder to move the file into, a path such as contracts/2026. Core tidies the path, so /contracts//2026/ is the same folder, and answers 422 on folder for a . or .. segment, a control character, or a path longer than 512 characters once tidied. An empty folder moves the file to the collection's root. A document a connector syncs keeps the folder its origin gives it: moving one answers 409, 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's uploads collection, and only into one your key reaches; anything else answers 422 on collection_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_file sends a note containing the file id and media type. It does not parse or extract the document.
  • Images: synchronous calls can send input_image with an uploaded image or inline data: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 optional detail (auto, low or high) 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_audio carries a recording, inline or by file_id. Core transcribes it before the model is called and the model reads the text; see Transcribe audio.
  • Images in a tool result: the output of a function_call_output can be a list of input_text and input_image parts 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}/content and a collection's GET .../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 with PATCH /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.

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, /artifacts or a collection's sources;
  • setting its retention back to window;
  • 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.