Skip to main content

Attachments and Media

Upload a general file as an attachment, then give the returned file reference to an Agent. Images, audio, and video can also be uploaded as media, with short-lived links for native media elements. A successful upload means the file was stored; it does not mean an Agent has read or processed it.

Prepare BASE_URL and an instance access token using Connect and Authenticate. BASE_URL is the target Agent Service address without /api; use HTTPS remotely. Except for signed media reads, requests on this page use X-Agent-Service-Token, with identity resolved by the service.

PurposeMethod and pathSuccess response
Store general files such as PDFs and textPOST /api/attachments201, attachment receipt
Download your attachmentGET /api/attachments/:attachmentId200, original file bytes
Store an image, audio, or video filePOST /api/media/upload200, media metadata
Obtain short links for readable mediaPOST /api/media/access-urls200, items and missing
Read media through a short linkGET /media/:filename200, or 206 for a single Range

Upload a general attachment​

POST /api/attachments​

Send one file as multipart/form-data. With curl --form, browser FormData, or the example below, let the tool generate the Content-Type including its boundary; do not set a bare multipart/form-data header.

curl --fail-with-body --silent --show-error \
--header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \
--form 'file=@./report.txt;type=text/plain' \
"${BASE_URL}/api/attachments"

Success returns the receipt directly, without a data or success wrapper:

{
"attachmentId": "00000000-0000-4000-8000-000000000001",
"name": "report.txt",
"mimeType": "text/plain",
"size": 18,
"sha256": "0932d7133fdafd99d58020fca2539266aa11731a72f741067b1009c4c8c66553",
"fileReference": "/srv/desirecore/attachments/owner/00000000-0000-4000-8000-000000000001/data"
}
FieldMeaning
attachmentIdService-issued UUID used for download, not a local filename
nameDisplay name with path separators and control characters removed, up to 240 characters
mimeTypeContent type description from upload, up to 128 characters
sizeComplete file size in bytes, an integer of at least 1
sha256Lowercase SHA-256 of the file bytes, 64 hexadecimal characters
fileReferenceFile reference on the target service; pass it unchanged in Query options.fileReferences on the same instance, rather than opening it on your own machine or constructing a path

This endpoint accepts general files without the media MIME list below. It stores files without extracting or executing them, and does not guarantee that a model or tool can parse their format. Passing a reference to an Agent does not automatically grant tool execution, editing, or cross-user read permission.

Upload one non-empty file per request, at most 20 MiB (20,971,520 bytes). Each user can retain up to 256 attachment directories and 512 MiB of file contents. Exceeding a quota returns 429. This page provides no attachment deletion endpoint. Each successful POST creates a new attachment, so do not repeatedly upload a file as an automatic retry. If no complete receipt arrives, treat the upload outcome as unknown and inspect the error before deciding whether to upload again.

Upload, then send to an Agent​

Download the attachment uploader as attachments.mjs. It needs Node.js 22+, with no dependencies to install. It checks the local file size and compares the successful receipt size and SHA-256 with the uploaded bytes. It does not retry POST automatically or overwrite an existing output file.

export DESIRECORE_URL='https://agent.example.com'
export DESIRECORE_TOKEN='replace-with-your-existing-instance-token'
export DESIRECORE_AGENT_ID='your-agent-id'

node attachments.mjs --file ./report.txt --mime text/plain --out ./attachment.json

attachment.json contains the direct receipt. Download agent-client.mjs and install its dependency as described in Conversations and Runs, then submit:

node agent-client.mjs \
--prompt 'Read the attachment and summarize its main points.' \
--attachment-receipt ./attachment.json \
--state ./request-with-attachment.json

Repeat --attachment-receipt to add several receipts. This example attaches files only to the initial start; subsequent --follow-up turns are text. Saved request state freezes the chosen references, so recovery and transport retries reuse them without uploading again. Receipts and state files can reveal file references or message contents; keep them private and use them with the original instance.

In your own client, place the receipt reference as shown below. originSocketId must be the current ID of an authenticated Socket.IO connection. Follow the conversation guide for submission, request digests, and terminal outcomes:

{
"prompt": "Summarize the attachment.",
"options": {
"agentId": "your-agent-id",
"conversationId": "your-agent-id",
"fileReferences": [
"/srv/desirecore/attachments/owner/00000000-0000-4000-8000-000000000001/data"
]
},
"clientRequestId": "00000000-0000-4000-8000-000000000002",
"userMessageId": "00000000-0000-4000-8000-000000000003",
"originSocketId": "current-authenticated-socket-id"
}

fileReference is not a public URL. HTTP 202 means the request was admitted; observe its terminal outcome or a correlated receipt to determine whether the attachment was processed.

Download a general attachment​

GET /api/attachments/:attachmentId​

Read the receipt's attachmentId using the same instance and user identity:

curl --fail-with-body --silent --show-error \
--header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \
--output ./downloaded-report.txt \
"${BASE_URL}/api/attachments/${ATTACHMENT_ID}"

Success returns file bytes with Content-Type: application/octet-stream, a filename in Content-Disposition, actual Content-Length, Cache-Control: no-store, and X-Content-Type-Options: nosniff. Do not parse it as JSON. This endpoint does not provide the media Range contract.

An invalid, missing, other-user, or unreadable attachment returns 404 attachment_not_found. Paths or attachment IDs in message history do not replace the download's identity check.

Upload images, audio, and video​

POST /api/media/upload​

Use single-file multipart again, with one of these MIME types:

CategoryMIME
Imageimage/jpeg, image/png, image/gif, image/webp
Audioaudio/mpeg, audio/mp3, audio/opus, audio/aac, audio/flac, audio/wav, audio/ogg
Videovideo/mp4, video/webm

Unsupported MIME types return 415; empty files return 400. This endpoint uses the service's multipart file limit of 2 GiB (2,147,483,648 bytes), rather than the general attachment limit of 20 MiB. Reverse proxies and deployments may impose smaller limits. Compress large files or shorten their duration before uploading.

curl --fail-with-body --silent --show-error \
--header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \
--form 'file=@./diagram.png;type=image/png' \
"${BASE_URL}/api/media/upload"
{
"type": "image",
"mediaId": "00000000-0000-4000-8000-000000000004.png",
"name": "diagram.png",
"mimeType": "image/png",
"size": 68,
"width": 1,
"height": 1
}

type is image, audio, or video; width and height appear only when image dimensions were obtained. Keep the metadata. To attach media to a Query, put it in the top-level mediaRefs array; general file references still belong in options.fileReferences. Upload success does not guarantee that the selected model supports the format.

New uploads belong to the authenticated user. Link issuance checks ownership and actual media contents, so guessing a mediaId is insufficient to obtain a link for another user's new media. Some media saved by older instances lacks ownership records and retains legacy read compatibility; do not treat historical media IDs as cross-user isolation credentials.

Obtain a signed link for native media​

POST /api/media/access-urls​

Send 1–100 mediaId values with the instance access token. Duplicates are removed in first-occurrence order, and extra top-level fields are rejected.

curl --fail-with-body --silent --show-error \
--header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \
--header 'Content-Type: application/json' \
--data '{"mediaIds":["00000000-0000-4000-8000-000000000004.png"]}' \
"${BASE_URL}/api/media/access-urls"
{
"items": [
{
"mediaId": "00000000-0000-4000-8000-000000000004.png",
"url": "/media/00000000-0000-4000-8000-000000000004.png?expires=1792112700&signature=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"expiresAt": "2026-10-16T01:05:00.000Z"
}
],
"missing": []
}

Use the url from an actual response, not the example signature above. It is relative to Agent Service; expiresAt is the expiry time. Missing files appear in missing, while other readable files can receive links. A malformed ID fails the entire request with 400. If any existing media is not readable by the user, the request returns 403; do not expect partial items in that response.

GET /media/:filename​

Give the returned URL to an <img>, <audio>, or <video> element:

// item comes from the authenticated /api/media/access-urls response.
image.src = new URL(item.url, BASE_URL).href;

This GET authenticates using expires and signature in the URL, without X-Agent-Service-Token. Send the long-lived token only in the issuance request header, never in a media URL. Links last five minutes, support repeated reads and single byte ranges, and permit anyone holding the complete link to read that media while valid. Do not publish them. Rotating the instance token invalidates old signatures; previously cached copies may remain until their cache entry expires.

This contract applies only to GET/HEAD on the single-segment /media/:filename path. It does not exempt /api/media/*, other methods, or multi-segment paths from instance authentication. Application connection credentials cannot use a media link to become an instance user. Browser origin restrictions and service readiness can still reject the request.

ReadResponse
Complete GET200, media bytes, extension-based content type, and length
Range: bytes=0-1023, or a valid open-ended/suffix range206, Content-Range, and segment length; one range only
Invalid or out-of-bounds range416, Content-Range: bytes */<file length>
HEADSame signature and metadata checks, without media response bytes

Successful reads include Accept-Ranges: bytes and X-Content-Type-Options: nosniff. Caching is private and lasts no longer than the signature's remaining validity. The default reverse proxy forwards /media/*; custom proxies must also route it to the same Agent Service.

Errors and recovery​

HTTP / errorAction
Attachment 400 attachment_file_required / attachment_emptySelect one non-empty file and upload as multipart
Attachment 401 attachment_identity_requiredCheck that the instance credential resolves the user; generic authentication may return 401 earlier
Attachment 413 attachment_too_large, or a parser 413Reduce the general attachment to 20 MiB or less and check proxy limits
Attachment 429 attachment_limit / attachment_storage_limitCount or storage quota reached; ask the administrator to resolve it instead of repeatedly uploading
Attachment 503 attachment_storage_unavailableStorage did not complete; retain the error and ask the administrator to check disk storage
Download 404 attachment_not_foundCheck the ID, original instance, and identity; another user's receipt cannot be used to read their file
Media 400 / 415Check IDs, request shape, non-empty bytes, and MIME; renaming an extension does not turn a file into valid media
Media 403 media_owner_forbiddenUse the uploader's identity or ask the owner to act within their permissions
Signed GET 401 media_signature_expired / invalid_media_signatureObtain a new link through the authenticated endpoint; do not edit the signature or expiry
Media 404 media_not_found / 416 range_not_satisfiableThe file may have been removed, or the Range needs correction
Media 503 media_signing_unavailableAsk the administrator to check the access-token configuration needed for signing

Internal media failures may return 500 media_internal_error. Upload parsers, reverse proxies, and generic authentication use different error bodies. Keep the HTTP status and actual response; see Errors and Troubleshooting.