> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.eyelevel.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.eyelevel.ai/_mcp/server.

# File Access and Sharing

GroundX file URLs are public and do not expire by default. On a compatible deployment, account, bucket and document policies can require authentication on every read. The URL stays the same and does not expire.

> **Availability**
>
> File protection requires a deployed file-access gateway, private storage and compatible API and processing services. Setting a policy during creation also requires compatible creation handlers and SDK models. Confirm support on your hosted account or on-prem deployment before using protected data. An API reference entry does not confirm availability. The REST examples below work without new SDK methods; check your SDK version before adapting them.

## Inheritance and lifetimes

Documents inherit bucket settings, and buckets inherit account settings. Use the existing resource GET operations to read `fileAccess` (the saved override), `effectiveFileAccess` (the combined policy) and `fileAccessRevision`.

| Setting              | Behavior                                                                                           |
| -------------------- | -------------------------------------------------------------------------------------------------- |
| Unconfigured account | Public stable URLs, with no URL expiry                                                             |
| `mode: "protected"`  | Stable URLs require the owner's current authentication on every read and do not expire             |
| `urlTtlSeconds`      | Maximum lifetime for new storage-signed share links; does not affect file retention or stable URLs |
| Omitted policy field | Inherits the corresponding ancestor setting                                                        |
| `fileAccess: null`   | Inherits on creation; clears this resource's override when updating with its current revision      |

A policy object needs `mode`, `urlTtlSeconds`, or both. A bucket or document cannot make content public if its account or bucket requires protection. It also cannot exceed an inherited share-link lifetime limit. A public account or bucket without an explicit lifetime imposes no such limit.

The default share-link lifetime is 300 seconds. Configured values range from 1 through 604800 seconds; storage credentials or provider limits may shorten the actual lifetime. Policies apply to GroundX-owned files. They cannot control previously downloaded copies or externally hosted originals.

## Create a bucket or files with a policy

Set the initial policy in the existing create request, without a revision. Omitted or null `fileAccess` inherits account or bucket settings. This example creates a bucket using an API key stored on your server:

```sh
curl --fail-with-body https://api.groundx.ai/api/v1/bucket \
  -H "X-API-Key: $GROUNDX_API_KEY" -H 'Content-Type: application/json' \
  --data '{"name":"private-documents","fileAccess":{"mode":"protected","urlTtlSeconds":300}}'
```

Remote ingest accepts `fileAccess` on each item in `documents`. Local multipart ingest accepts it in each item's metadata. Website crawl accepts it on each item in `websites`. Crawled documents and split files retain their original item's policy.

```json
{
  "documents": [
    {"bucketId": 1234, "sourceUrl": "https://files.example/private/reservation-a.pdf", "fileType": "pdf", "fileAccess": {"mode": "protected", "urlTtlSeconds": 120}},
    {"bucketId": 1234, "sourceUrl": "https://files.example/private/reservation-b.pdf", "fileType": "pdf", "fileAccess": {"mode": "protected", "urlTtlSeconds": 300}}
  ]
}
```

Submit this body to `POST /v1/ingest/documents/remote` with `X-API-Key`. Replace the example URLs with the `hostedUrl` values from the private-upload flow below. Each item keeps its own policy. Poll the returned `ingest.processId` through `GET /v1/ingest/{processId}` and inspect individual document status before search. An ingest response does not confirm successful processing.

For a protected file in a public bucket, use private upload **before sending the file**, then include its protected policy during ingest. Uploading publicly and protecting the document afterward leaves the original upload exposed. GroundX must be able to read remote source URLs; setting a policy does not add support for authenticated third-party URLs.

## Change existing settings

Read the resource's current revision first. Replace or clear only that resource's override with the revision you read:

```json
{"fileAccess":{"mode":"protected","urlTtlSeconds":300},"fileAccessRevision":0}
```

Use `PUT /v1/customer` for your account or `PUT /v1/bucket/{bucketId}` for a bucket. The account endpoint accepts only its file-access settings. A bucket policy update cannot include a rename. Document updates use the existing request body:

```json
{"documents":[{"documentId":"11111111-1111-4111-8111-111111111111","fileAccess":null,"fileAccessRevision":1}]}
```

Submit that body to `PUT /v1/ingest/documents`. A policy update contains exactly one document, with no rename, metadata or callback fields. It does not trigger extraction or indexing.

Omitting `fileAccess` leaves settings unchanged. Supplying a policy replaces the override; fields omitted from that replacement inherit account or bucket settings. Setting `fileAccess` to null clears the override and requires the current revision. Read the resource again to confirm the saved settings.

## Read a protected stable URL

Use the returned hosted URL on your trusted server, with `X-API-Key` on every read. Use only configured GroundX file hosts; never forward credentials to arbitrary document URLs or across redirects. An authorized delegated Partner request also uses its normal `X-Customer-Key` selection. API keys belong in server-side configuration, never query strings, browser code, logs or chat.

```python
import os
import requests

# stable_url is a returned URL on an allowlisted GroundX file host.
response = requests.get(
    stable_url,
    headers={"X-API-Key": os.environ["GROUNDX_API_KEY"]},
    allow_redirects=False,
    timeout=30,
)
response.raise_for_status()
```

Browser applications should use their authenticated application backend to authorize access and either stream the content or issue a short share link. An MCP OAuth token is not a file-host credential. Stable raw URLs support GET/HEAD, byte-range requests and conditional requests. GroundX checks authorization before returning content or confirming that a cached copy is current.

## Share a file, page or crop

Call `POST /v1/ingest/document/{documentId}/file-link` with `X-API-Key` and a selector for a saved file, page or crop your account owns:

```json
{"asset":{"kind":"page","pageNumber":1,"corrected":false},"expiresInSeconds":120}
```

The response is `{ "url": "...", "expiresAt": "..." }`. `expiresInSeconds` can shorten the effective lifetime but cannot exceed it. Use the returned URL unchanged. The recipient needs no GroundX credentials.

Anyone with the link can reuse or forward it until expiry. Revoking an API key or changing a policy does not revoke an existing storage-signed link. A link signed for GET does not permit HEAD or PUT.

| Asset                     | Selector                                           |
| ------------------------- | -------------------------------------------------- |
| Stored original           | `{"kind":"source"}`                                |
| Stored raw X-Ray JSON     | `{"kind":"xray"}`                                  |
| Primary extraction output | `{"kind":"extract"}`                               |
| Saved page                | `{"kind":"page","pageNumber":1,"corrected":false}` |
| Saved crop by chunk       | `{"kind":"crop","chunkId":"saved-chunk-id"}`       |
| Saved crop by position    | `{"kind":"crop","pageNumber":1,"chunkIndex":0}`    |

Use saved page numbers starting at 1. Chunk indices start at 0 within each saved page. Identify a crop by either chunk ID or page and chunk index. Raw URLs, object keys, owner selectors and unrelated fields are rejected.

## Upload source files

Use the same upload endpoint for public and protected files, hosted and on-prem.
It remains available when file protection is disabled and signs against the
deployment's configured storage. SDKs, widgets and MCP use their configured GroundX
API. File bytes go directly to storage. Legacy anonymous upload formats remain
compatibility adapters to the same signer; clients do not fall back to a hosted broker.

1. Call `POST /v1/bucket/{bucketId}/file-upload` with `X-API-Key` and `{"fileName":"invoice.pdf","fileType":"pdf"}` for a public upload. For a protected file, include `"fileAccess":{"mode":"protected"}` in this request and the later ingest request.
2. The `201` response supplies `uploadId`, `uploadUrl`, `hostedUrl`, `expiresAt`, `method: "PUT"` and `headers`.
3. PUT the bytes directly to `uploadUrl`, unchanged, with exactly the returned required headers. Do not send GroundX keys or customer headers to the storage service.
4. For a private reservation, before reservation expiry, ingest `hostedUrl` into the same owned bucket, including the requested initial file policy. Keep the upload reservation and document under the same customer selection.
5. Poll ingest status and read the document's confirmed policy.

The upload endpoint validates the requested file policy before returning any storage
grant. A non-null policy returns 403 when protection is disabled, before bytes leave
the client. Omitted or null policy inherits.

PUT must finish before the upload grant expires. Public uploads have no private reservation,
even when the file-protection feature is enabled. Expiry of the PUT grant does not expire the
uploaded source or prevent later ingest. The reply's `effectiveFileAccess` reports the
policy used at issuance. Only protected uploads need an ingest deadline; older replies
without this field should retain protected-upload checks.

The private upload reservation is tied to its bucket and owner. The upload link lasts at most 900 seconds, regardless of the read-share lifetime. Storage credentials may shorten it. If it expires, request a new reservation and upload again. Do not use public upload as a fallback.

An MCP host must be able to read the local file and perform HTTP PUT to complete this upload. Private upload also supports protected files in public buckets.

## X-Ray and search links

For protected documents, returned `xrayUrl` uses `?view=links`. Authenticate as the owner to get fresh links to the document's stored source, pages and crops. GroundX updates the links when serving this JSON; it does not store a new copy. This view requires GET, rejects Range and returns fresh JSON without raw-file ETags or 304 responses. Large responses stream through the file gateway, outside the buffered API.

The X-Ray URL without `?view=links` returns the stored raw JSON. An `xray` share link shares only that JSON, without granting access to the files it references. Preserve the raw file when exact bytes matter, such as for fixtures or evidence. The existing `getXray` API remains available within its response-size limits.

For protected owned assets, search `sourceUrl`, `multimodalUrl`, `pageImages` and `pages[].imageUrl` contain storage-signed links that expire within the effective share-link lifetime. Ordinary public stable links remain unchanged. Retain `documentId`, `chunkId` and saved page numbers separately. When a link expires, call `documents.fileLink` (`POST /v1/ingest/document/{documentId}/file-link`) with the source, crop or page selector from those identifiers, or re-run an authorized search. For document review, fetch `xrayUrl` again. A failed link is not a reason to make the resource public or expose a key in the browser.

## Errors and deployment prerequisites

| Result                                        | Action                                                                                                                                                                      |
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`                                         | Fix the request body, missing update revision, selector, lifetime or inherited policy restriction                                                                           |
| `401` / `403` / `404`                         | Check environment, owner, credentials and resource visibility; do not retry anonymously                                                                                     |
| `409`                                         | Re-read settings for a stale revision, or resolve the reported ownership conflict or missing file protection                                                                |
| `404` when requesting a share link            | File access is disabled, or the owned resource or asset is absent; confirm deployment support and resource visibility. Upload issuance remains available without protection |
| `403` when creating or updating with a policy | File access is disabled, or the request is forbidden; confirm deployment support and permissions                                                                            |
| `503` with `Retry-After`                      | File access is enabled but policy checks, ownership checks or storage are temporarily unavailable; wait for the stated delay and read the saved settings                    |
| Storage rejects an expired link               | Request a new link through the authenticated API                                                                                                                            |

Policy updates are saved in one transaction. If it fails before commit, the previous settings and revision remain. After a timeout or lost response, read the settings before retrying because the update may have succeeded. Reusing an old revision then returns `409`. Updates do not remain pending or recover in the background.

On-prem deployments use the same API requests and responses with local API and file endpoints. They require:

* Compatible API, processor and file-gateway images.
* Private storage for reads and writes, with reachable storage-signed URLs.
* TLS certificates and trust settings.
* Shared policy, cache and database configuration.
* Verified protection for existing files before applying a policy to them.

Each supported storage backend needs acceptance tests against its exact version. S3 compatibility alone does not certify MinIO, AIStor or S4. Hosted support does not confirm on-prem support. Local deployments may not provide hosted OAuth discovery. See [GroundX On-Prem on AWS](/documentation/ground-x-on-prem/ground-x-on-prem-on-aws) and [Debugging GroundX On-Prem](/documentation/ground-x-on-prem/debugging-ground-x-on-prem).