Downloading Content
This tutorial shows how to get a document's binary content out of the Cloud Content Repository. You can stream it through the API, or ask for a temporary pre-signed URL that a browser or another service can fetch without a bearer token. It also covers range requests, conditional re-downloads, integrity digests, and the Accept header rules that decide which errors you see.
Prerequisites
- A valid user access token for the signed-in user you are searching for, not a service user token: document ACLs are applied to the caller (see Authentication)
- The base URL of your environment
- The
sys_idof a document with a blob, for example from a query or thesysembed_docIdof a vector-search hit
Set two variables before running the curl examples. The HTTP examples use the equivalent {{baseUrl}} and {{accessToken}} placeholders.
export BASE_URL="https://{environment_id}.content-lake.ai.experience.hyland.com"
export TOKEN="<access token of the signed-in user>"
Accept header matters on these endpointsDownload endpoints don't produce JSON. If you send the JSON Accept: application/vnd.hxcs.v1+json header used by the other APIs, you get 406 {"entity-type":"exception","status":406,"message":"No acceptable representation"}. Use Accept: */* for the pre-signed URL endpoint, and */* or application/octet-stream for the stream endpoint (see the error notes below for why */* is safer).
Tutorial Overview
In this tutorial, you'll learn to:
- Choose between streaming and a pre-signed URL
- Get a pre-signed download URL
- Stream the file, a byte range, or only a changed file
- Read the response headers
- Recognize the common errors
Choose a Download Method
| Method | Endpoint | Use when |
|---|---|---|
| Pre-signed URL | GET /api/download/url/{docId}/{propertyXPathAndFilename} | Something other than your service fetches the file (a browser, a viewer, a downstream job). The URL works without the bearer token until it expires. |
| Stream | GET /api/download/{docId}/{propertyXPathAndFilename} | Your service reads the bytes itself. Supports Range, conditional requests and content digests. |
In both endpoints, propertyXPathAndFilename is the XPath to the property holding the blob (e.g., sysfile_blob, cin_blobs/0/blob or sysfiles_blobs/0). On the stream endpoint you can append a filename (e.g., sysfile_blob/contract.docx or cin_blobs/0/blob/statement.pdf) to set the download filename; the pre-signed URL endpoint ignores it. Without a filename, the blob's own filename is used, which for ingested documents can be a generated ID rather than the original filename.
Get a Pre-signed Download URL
Returns a temporary pre-signed S3 URL for downloading a blob. The URL can be used without the bearer token until it expires.
- HTTP
- curl
GET {{baseUrl}}/api/download/url/{docId}/sysfile_blob
Authorization: Bearer {{accessToken}}
Accept: */*
curl -X GET "$BASE_URL/api/download/url/{docId}/sysfile_blob" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: */*"
Response
200 with Content-Type: text/plain;charset=UTF-8. The body is the URL as plain text, not a JSON object. The signed URL carries the filename in response-content-disposition and the lifetime in X-Amz-Expires.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
expiration | integer | No | URL lifetime in seconds. Defaults to the server's configured lifetime (3600 seconds in the tested environment), which is also the maximum: larger values are silently capped, and 0 or negative values use the default. A non-numeric value returns 400. Once the URL expires, the storage service returns 403 (Request has expired). |
inline | boolean | No | When true, the signed URL serves the content with Content-Disposition: inline instead of attachment. Honoured only for PDF and images. Other types, including text/plain, Office documents, JSON, XML, HTML and application/octet-stream, are always served as attachment. |
Headers
| Header | Required | Description |
|---|---|---|
Accept | No | */* is recommended (omitting Accept works the same). A JSON type returns 406 with a JSON body; text/plain returns 406 with an empty body. With application/octet-stream, errors return an empty 500 instead of the real 404. |
HXCS-REPOSITORY | No | Repository ID (default: default) |
Examples
1. Default lifetime, served as an attachment
curl -H "Authorization: Bearer <token>" -H "Accept: */*" \
"https://{environment_id}.content-lake.ai.experience.hyland.com/api/download/url/{docId}/sysfile_blob"
# 200 text/plain
https://clake-{bucket}.s3.us-east-1.amazonaws.com/storage/{digest}?response-content-disposition=attachment%3B%20filename%3Dcontract.docx&X-Amz-Algorithm=AWS4-HMAC-SHA256&...&X-Amz-Expires=3600&X-Amz-Signature=...
2. Short-lived URL for in-browser viewing
curl -H "Authorization: Bearer <token>" -H "Accept: */*" \
"https://{environment_id}.content-lake.ai.experience.hyland.com/api/download/url/{docId}/sysfile_blob?expiration=60&inline=true"
# 200 text/plain — for a PDF blob, the URL contains:
...response-content-disposition=inline%3B%20filename%3Dcontract.pdf...&X-Amz-Expires=60...
3. Wrong Accept header
curl -H "Authorization: Bearer <token>" -H "Accept: application/vnd.hxcs.v1+json" \
"https://{environment_id}.content-lake.ai.experience.hyland.com/api/download/url/{docId}/sysfile_blob"
# 406
{"entity-type":"exception","status":406,"message":"No acceptable representation"}
Errors
| Status | Body | When |
|---|---|---|
401 | (empty) | Missing or invalid token |
404 | {"entity-type":"exception","status":404,"message":"No document found"} | Unknown docId |
404 | {"entity-type":"exception","status":404,"message":"No blob found"} | The document exists but has no blob at that property (e.g., a typo such as nosuch_blob) |
406 | {"entity-type":"exception","status":406,"message":"No acceptable representation"} | Accept is a JSON type |
406 | (empty) | Accept is text/plain |
500 | (empty) | Unknown document or blob and Accept: application/octet-stream. Send */* to get the real 404. |
501 | Pre-signed URL not available for this blob provider | A pre-signed URL is not available for the storage provider holding this blob |
Stream the File
Streams the blob directly as binary content. Supports range requests, conditional requests and content digests.
- HTTP
- curl
GET {{baseUrl}}/api/download/{docId}/sysfile_blob
Authorization: Bearer {{accessToken}}
Accept: */*
curl -X GET "$BASE_URL/api/download/{docId}/sysfile_blob" -o contract.docx \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: */*"
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
inline | boolean | No | When true, sends Content-Disposition: inline; filename=... instead of attachment; filename=.... Honoured for PDF, images and text/plain; other types, including Office documents, JSON, XML, HTML and application/octet-stream, are always served as attachment. |
Request Headers
| Header | Required | Description |
|---|---|---|
Accept | No | */* (recommended; omitting it works the same), application/octet-stream, application/vnd.hxcs+octet-stream or application/vnd.hxcs.v1+octet-stream. A JSON Accept returns 406 with a JSON body; other types such as text/plain or application/pdf return 406 with an empty body. |
HXCS-REPOSITORY | No | Repository ID (default: default) |
Range | No | Request a byte range (e.g., bytes=0-99 or a suffix such as bytes=-500); returns 206 Partial Content with Content-Range. A range beyond the end of the blob returns 200 with the full content. |
If-None-Match | No | An ETag from a previous response (or *); returns 304 with no body if it matches |
If-Modified-Since | No | A timestamp in epoch milliseconds (the format of this API's Last-Modified header); returns 304 if the document has not been modified since. Last-Modified follows the document's sys_modified, so it changes when the document is updated (for example by enrichment), not only when the file changes. |
Want-Digest | No | e.g. sha-256. Adds a Digest response header when the blob's digest algorithm matches |
Response Headers and Status Codes
The body is the binary content, with Content-Type set from the blob's MIME type (application/vnd.hxcs.v1+octet-stream if the blob has none). The response headers are:
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
Content-Length: 59049
ETag: "5d7ce8a87cb3809a08f1d3dacc84e7bf5504cb603e7703f7d35056092634cf47"
Last-Modified: 1790853760535
Content-Disposition: attachment; filename=contract.docx
Accept-Ranges: bytes
Last-Modified is in epoch milliseconds, not an HTTP date. Content-Range is added for range requests and Digest when requested. Every response also carries X-Content-Type-Options: nosniff and Cache-Control: no-cache, no-store, max-age=0, must-revalidate; a 304 instead carries Cache-Control: max-age=15, must-revalidate.
| Status | Description |
|---|---|
200 | Full content returned |
206 | Partial content returned for a Range request |
304 | Not modified (If-None-Match / If-Modified-Since matched); no body |
401 | Missing or invalid token (empty body) |
404 | Document or blob not found. Returned only when Accept allows JSON, such as */*; the body is {"entity-type":"exception","status":404,"message":"No document found"} |
406 | Accept is a JSON type (JSON body) or another non-binary type (empty body) |
500 | Document or blob not found and Accept: application/octet-stream: the error can't be rendered in that media type, so you get an empty 500 instead of the 404 |
Accept: */* to see real errorsWith Accept: application/octet-stream, an unknown docId or a missing blob comes back as an empty 500. With Accept: */*, the same request returns 404 with a JSON message.
Examples
1. Download the file
curl -H "Authorization: Bearer <token>" -H "Accept: */*" -o contract.docx \
"https://{environment_id}.content-lake.ai.experience.hyland.com/api/download/{docId}/sysfile_blob"
# 200, Content-Disposition: attachment; filename=contract.docx
2. First 100 bytes only
curl -H "Authorization: Bearer <token>" -H "Accept: */*" -H "Range: bytes=0-99" \
"https://{environment_id}.content-lake.ai.experience.hyland.com/api/download/{docId}/sysfile_blob"
# 206, Content-Range: bytes 0-99/59049, Content-Length: 100
3. Re-download only if changed
curl -H "Authorization: Bearer <token>" -H "Accept: */*" \
-H 'If-None-Match: "5d7ce8a87cb3809a08f1d3dacc84e7bf5504cb603e7703f7d35056092634cf47"' \
"https://{environment_id}.content-lake.ai.experience.hyland.com/api/download/{docId}/sysfile_blob"
# 304, no body
4. Inline with an integrity digest
curl -H "Authorization: Bearer <token>" -H "Accept: */*" -H "Want-Digest: sha-256" \
"https://{environment_id}.content-lake.ai.experience.hyland.com/api/download/{docId}/sysfile_blob?inline=true"
# 200 for a PDF blob: Content-Disposition: inline; filename=contract.pdf
# Digest: SHA-256=XXzoqHyzgJoI8dPazITnv1UEy2A+dwP301BWCSY0z0c=
5. Unknown document
# Accept: */*
404 {"entity-type":"exception","status":404,"message":"No document found"}
# Accept: application/octet-stream
500 (empty body)
HEAD variant: HEAD /api/download/{docId}/{propertyXPathAndFilename} returns 204 with Content-Disposition, ETag, Last-Modified and Accept-Ranges, but no Content-Type, Content-Length or body. With a Range header it returns 206 with Content-Range. It can also return 304 or 404, as above.
Next Steps
- Semantic and Hybrid Search: find the chunks, then download their source documents
- Querying with HXQL: find the documents to download
- API Reference: every endpoint and schema
- Error Reference: error formats and status codes