# Files

[Open in the interactive reference](https://docs.api.lensing.ai/guides/files)

In practice: a file field is a string on the wire. In a request it takes an **upload reference** you get by uploading the file first; in a response it holds a **handle** you exchange for the file. Both are short-lived and opaque, and neither is the file's id.

The OpenAPI document marks a file field as `{ "type": "string", "x-file": true }`, and this reference shows its type as `file`. Filters and data-set parameters stay plain strings.

### Uploading a file

1. Send the file as `multipart/form-data` to `POST /public/files/{namespace}/{operationId}`, with the `Api-Version` header. `operationId` names the endpoint you will call with it; `namespace` is the segment after `/public/api/` in your base URL. Name each file part as you like: the answer maps each name to an upload reference under `data`.
2. Call the endpoint and pass the reference as the file field's value. The platform swaps it for the stored file before the endpoint runs.

- A reference works only for the user who uploaded it and only for the endpoint it was uploaded for, and expires after a few minutes. Upload right before the call.
- A file field accepts nothing else. A handle you read from a record, an id or an expired reference is answered with `400` `Invalid input`, the field's path in `diagnostics`, and the endpoint does not run. To keep a file a record already has, leave the field out.
- One request takes up to 10 files of at most 15 MB each, under distinct part names.
- With an `Idempotency-Key`, a retry replays the stored response even after its reference has expired. The key covers the body as sent, so a retry that sends a new reference for the same file is a different request; see [Idempotency](https://docs.api.lensing.ai/guides/idempotency.html).

### Downloading a file

Send a handle to `GET /public/files/{handle}` for the file's content, or to `GET /public/files/{handle}/metadata` for its name, type and size. Fetch the file soon after reading the record.

- A handle works for the user your access token acts as, with any of that user's tokens. For a system access token, that is the integration's system user. Another user's handle is answered with `404`.
- It works only while you may still call the endpoint that returned it. Once that grant is gone, the handle is answered with `404`.
- It expires after a short time, and stops working when the file is removed. Don't store handles: when you need the file again, read the record again for a new one.
- Treat it as opaque. Reading the same record twice can return two different handles for the same file, so don't compare handles or read anything out of them.

### The file endpoint

`/public/files` is on the same host as the API, but not under `/public/api/<namespace>`. It takes the same `Authorization` header as every other request. Uploads need `Api-Version`; downloads don't.

- `GET /public/files/{handle}` returns the content. `Content-Type` is the file's type, or `application/octet-stream` when it is unknown, and `Content-Disposition: attachment` carries the file name.
- `HEAD /public/files/{handle}` answers with the same headers and a `Content-Length`, without the content.
- `GET /public/files/{handle}/metadata` returns `fileName`, `mimeType` and `size` in bytes under `data`. `mimeType` and `size` are left out when they are unknown.

Every answer carries `Cache-Control: private, no-store`. Let your client follow redirects, and don't send the `Authorization` header on to another host: the content may be served from a different location in a later release.

### Status codes

| Status | Meaning, and what to do |
| --- | --- |
| `200` | The upload references, the content, or the metadata. |
| `400` | An upload without `Api-Version`, with more than 10 files, with two parts of the same name, or a body that is not valid `multipart/form-data`. On the endpoint call: a file field that holds anything but a valid upload reference. |
| `401` | No valid access token, as on every request; see [Authentication](https://docs.api.lensing.ai/guides/auth.html). |
| `403` | An upload for an endpoint your token may not call. Uploading needs the same permission as the call itself. |
| `404` | On an upload: no endpoint of that name, or one without a file field. On a download: the handle is unknown or expired, belongs to another user, comes from an endpoint you may no longer call, or the file was removed. The answer is the same for all of them on purpose. Read the record again for a new handle; sending the same handle again will not help. |
| `413` | A file larger than 15 MB. |
| `429` | Too many requests, as on every request; see [Rate limits and retries](https://docs.api.lensing.ai/guides/limits.html). |

## Examples

**1. Upload the file**

```
curl -X POST 'https://<your-tenant-host>/public/files/<namespace>/createLibraryBook' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Api-Version: v1' \
  -F 'cover=@cover.svg;type=image/svg+xml' \
  -F 'sampleChapter=@chapter.pdf;type=application/pdf'

HTTP/1.1 200 OK
Cache-Control: private, no-store

{ "data": { "cover": "u1.fIrOBkgwx7mXZLV3PekHTg.1nIoVCd…",
            "sampleChapter": "u1.QzsD8qBFrYJ-MoO0b_iehQ.KArJ_qL0…" } }
```

**2. Send the references as the file fields**

```
curl -X POST '<base URL>/library/books' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Api-Version: v1' \
  -H 'Content-Type: application/json' \
  -d '{ "title": "The Curl Handbook", …,
        "cover": "u1.fIrOBkgwx7mXZLV3PekHTg.1nIoVCd…",
        "sampleChapter": "u1.QzsD8qBFrYJ-MoO0b_iehQ.KArJ_qL0…" }'

HTTP/1.1 200 OK

{ "data": { "id": "d04e243e-002c-4fdc-b48b-1692e098c28d" } }
```

**3. Read the record: each file field is a handle**

```json
{
  "data": {
    "id": "d04e243e-002c-4fdc-b48b-1692e098c28d",
    "title": "The Curl Handbook",
    "cover": "v1.PSa6ZvU-WUeMWfgZMo_oFg.b63x0zr…",
    "sampleChapter": "v1.jNEmR2brBKl2Zi8GhcFZnQ.r7Kgu7X…"
  }
}
```

**4. Read a file's metadata, or download it**

```
curl 'https://<your-tenant-host>/public/files/'"$FILE_HANDLE"'/metadata' \
  -H "Authorization: Bearer $ACCESS_TOKEN"

{ "data": { "fileName": "chapter.pdf", "mimeType": "application/pdf", "size": 37 } }

curl -OJ 'https://<your-tenant-host>/public/files/'"$FILE_HANDLE" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 37
Content-Disposition: attachment; filename=chapter.pdf
Cache-Control: private, no-store
X-Content-Type-Options: nosniff
```

**Anything but an upload reference in a file field**

```json
{
  "error": {
    "message": "Invalid input",
    "diagnostics": [
      {
        "path": [
          "cover"
        ],
        "message": "Expected a file reference returned by the file upload endpoint for this action, to this caller, that has not expired"
      }
    ]
  }
}
```

**A handle that is expired, unknown or not yours**

```json
{
  "error": {
    "message": "The requested file could not be found"
  }
}
```
