# The Endorr Cloud API

> Put Endorr Cloud inside your own apps, shops, scripts and AI agents. One API key, plain HTTP, and paths you already know: upload a file in one request.

Source: https://endorr.com/developers

Everything is JSON over HTTPS at `https://auth.endorr.com/v1/cloud`. There is no SDK to install, no OAuth dance, and no separate upload host to learn. AI agents can use the same key through the [MCP server](https://endorr.com/developers#agents) or the [OpenAPI document](https://auth.endorr.com/v1/cloud/openapi.json).

## Contents

- [Quickstart](#quickstart)
- [AI agents and MCP](#agents)
- [API keys](#auth)
- [Paths and ids](#paths)
- [Upload a file](#upload)
- [Download a file](#download)
- [List a folder](#list)
- [Create folders](#folders)
- [Rename, move, delete](#change)
- [Share links](#share)
- [Search](#search)
- [Large files](#large)
- [Errors](#errors)
- [Limits](#limits)
- [Endpoint reference](#reference)

## Quickstart

1. In Endorr Studio, open [Settings, API keys](https://endorr.com/studio/settings/api-keys) and create a key. Give it **Read files** and **Add and change files**.
2. Copy the key. It starts with `endorr_sk_` and is shown once.
3. Upload your first file:

curl:

```bash
export ENDORR_KEY=endorr_sk_...

curl -T invoice.pdf \
  -H "Authorization: Bearer $ENDORR_KEY" \
  "https://auth.endorr.com/v1/cloud/files?path=/Invoices/2026/invoice.pdf"
```

JavaScript:

```javascript
import fs from 'node:fs';

const res = await fetch('https://auth.endorr.com/v1/cloud/files?path=/Invoices/2026/invoice.pdf', {
  method: 'PUT',
  headers: { Authorization: `Bearer ${process.env.ENDORR_KEY}` },
  body: await fs.promises.readFile('invoice.pdf'),
});
const { file } = await res.json();
console.log(file.id, file.path);
```

Python:

```python
import os, requests

with open("invoice.pdf", "rb") as f:
    r = requests.put(
        "https://auth.endorr.com/v1/cloud/files",
        params={"path": "/Invoices/2026/invoice.pdf"},
        headers={"Authorization": f"Bearer {os.environ['ENDORR_KEY']}"},
        data=f,
    )
print(r.json()["file"]["id"])
```

That is the whole flow. The `Invoices` and `2026` folders are created for you, and the answer tells you the file's id and where it landed:

```json
{
  "file": {
    "type": "file",
    "id": "file_01JB8Q...",
    "name": "invoice.pdf",
    "path": "/Invoices/2026/invoice.pdf",
    "folder_id": "fld_01JB8Q...",
    "size_bytes": 48213,
    "content_type": "application/pdf",
    "sha256": "9f2c...",
    "status": "ready",
    "created_via": "api",
    "created_at": "2026-10-21T09:30:00.000Z",
    "updated_at": "2026-10-21T09:30:00.000Z"
  },
  "replaced": false
}
```

## AI agents and MCP

The API is built to be used by agents as much as by people. Point an agent at any of these and it can work out the rest:

| What | Where |
| --- | --- |
| MCP server (Streamable HTTP) | `https://auth.endorr.com/v1/cloud/mcp` |
| OpenAPI 3.1 document | `https://auth.endorr.com/v1/cloud/openapi.json` |
| These docs as one Markdown file | `https://endorr.com/developers.md` |
| Index for LLMs | `https://endorr.com/llms.txt` and `https://endorr.com/llms-full.txt` |
| Discovery (no key needed) | `GET https://auth.endorr.com/v1/cloud` |

**Connect an agent over MCP.** Create a key with only the permissions the agent needs, ideally limited to one folder, and add the server with the key as a bearer token. In Claude Code:

```bash
claude mcp add --transport http endorr-cloud https://auth.endorr.com/v1/cloud/mcp \
  --header "Authorization: Bearer endorr_sk_..."
```

Clients configured with JSON (Claude Desktop, Cursor, VS Code and others) take an entry like this:

```json
{
  "mcpServers": {
    "endorr-cloud": {
      "type": "http",
      "url": "https://auth.endorr.com/v1/cloud/mcp",
      "headers": { "Authorization": "Bearer endorr_sk_..." }
    }
  }
}
```

The agent sees only the tools its key allows:

| Tool | Does | Needs |
| --- | --- | --- |
| `whoami` | Who the key acts for, its permissions and folder, storage | `files.read` |
| `list_folder` | List a folder by path or id | `files.read` |
| `search` | Find files and folders by name | `files.read` |
| `get_file_info` | A file's details | `files.read` |
| `read_file` | Text and images inline (up to 1 MB), else a download link | `files.read` |
| `get_download_link` | A short-lived link to the bytes | `files.read` |
| `write_file` | Create or overwrite a file from text or base64 | `files.write` |
| `create_folder` | Create a folder and missing parents | `files.write` |
| `move_or_rename` | Rename or move a file or folder | `files.write` |
| `move_to_trash` | Trash a file or folder (restorable for 28 days) | `files.delete` |
| `create_share_link` | A public view-only link | `shares.create` |

**Teach a coding agent the API.** Paste this into your project's `AGENTS.md` or `CLAUDE.md`:

```markdown
## Endorr Cloud
Files live in Endorr Cloud. API docs: https://endorr.com/developers.md
- Base URL: https://auth.endorr.com/v1/cloud
- Auth: `Authorization: Bearer $ENDORR_KEY` (never commit the key)
- Upload: `PUT /v1/cloud/files?path=/folder/name.ext` with the file as the body
- List: `GET /v1/cloud/folders?path=/folder`; download: `GET /v1/cloud/files/download?path=…` (follow the redirect)
- Errors are `{ "error": { "code", "message", "docs_url" } }`; branch on `code`
```

> **Give agents the least they need.** A key limited to one folder with only `files.read` cannot see or change anything else. Agents may not trash files or create public links unless you give the key `files.delete` or `shares.create`.

## API keys

Send your key on every request as `Authorization: Bearer endorr_sk_…`. Check a key with `GET /v1/cloud/me`: it answers who the key acts for, what it may do and how much storage is used.

A key acts as you, inside the workspace you made it in. It can never do more than you can, and only what you allowed when you made it:

| Permission | Allows |
| --- | --- |
| `files.read` | List, search and download files and folders |
| `files.write` | Upload files, create folders, rename and move |
| `files.delete` | Move files and folders to the trash (restorable for 28 days) |
| `shares.create` | Create view-only share links |

Limit a key to one folder and it sees that folder as `/`, and nothing outside it. That is the safest choice for an app or agent that only needs a place of its own. You can change a key's permissions later without changing the key, set an expiry date, and revoke it at any time; it stops working at once.

> **Keep keys on your server.** A key is like a password. Never put it in a web page, mobile app or public repository. If a key leaks, revoke it in Studio and make a new one.

## Paths and ids

Use whichever is easier. Every file and folder can be reached by its **path**, like `/Clients/Acme/logo.png`, or by its **id**, like `file_01JB8Q…` or `fld_01JB8Q…`. Ids never change when you rename or move something, so store ids if you keep references.

Paths start at `/`, are case-sensitive, and never contain `.` or `..`. Encode them in a query string as usual (`?path=/My%20files/a.pdf`). The folder id `root` also means `/`.

## Upload a file

`PUT /v1/cloud/files?path=…` with the file as the request body, up to 100 MB. `POST` works too. The content type comes from the file extension, or from your `Content-Type` header for extensions we do not know.

When a file with the same name is already there, `on_conflict` decides:

- `rename` (default): keep both. The new one becomes `invoice (2).pdf`. Nothing is ever lost.
- `overwrite`: replace the old file's contents. It keeps its id and share links. The answer has `"replaced": true`.
- `fail`: answer `409 already_exists` with the existing file's id in `details.file_id`.

```bash
curl -T report.pdf -H "Authorization: Bearer $ENDORR_KEY" \
  "https://auth.endorr.com/v1/cloud/files?path=/Reports/weekly.pdf&on_conflict=overwrite"
```

## Download a file

`GET /v1/cloud/files/{id}/download`, or by path with `GET /v1/cloud/files/download?path=…`. The answer is a redirect to a short-lived link to the bytes, so tell your client to follow redirects. Add `redirect=false` to get `{ "url", "expires_at" }` instead.

curl:

```bash
curl -L -o invoice.pdf -H "Authorization: Bearer $ENDORR_KEY" \
  "https://auth.endorr.com/v1/cloud/files/download?path=/Invoices/2026/invoice.pdf"
```

JavaScript:

```javascript
const res = await fetch('https://auth.endorr.com/v1/cloud/files/download?path=/Invoices/2026/invoice.pdf', {
  headers: { Authorization: `Bearer ${process.env.ENDORR_KEY}` },
}); // fetch follows the redirect
const bytes = Buffer.from(await res.arrayBuffer());
```

Python:

```python
r = requests.get(
    "https://auth.endorr.com/v1/cloud/files/download",
    params={"path": "/Invoices/2026/invoice.pdf"},
    headers={"Authorization": f"Bearer {os.environ['ENDORR_KEY']}"},
)  # requests follows the redirect
open("invoice.pdf", "wb").write(r.content)
```

File details without the bytes: `GET /v1/cloud/files/{id}` or `GET /v1/cloud/files?path=…`.

## List a folder

`GET /v1/cloud/folders?path=/Invoices` or `GET /v1/cloud/folders/{id}`. One list of `items`, folders first, then files, by name. Each item has a `type` of `folder` or `file`.

curl:

```bash
curl -H "Authorization: Bearer $ENDORR_KEY" "https://auth.endorr.com/v1/cloud/folders?path=/Invoices"
```

JavaScript:

```javascript
let cursor = null;
do {
  const url = new URL('https://auth.endorr.com/v1/cloud/folders');
  url.searchParams.set('path', '/Invoices');
  if (cursor) url.searchParams.set('cursor', cursor);
  const page = await (await fetch(url, { headers: { Authorization: `Bearer ${process.env.ENDORR_KEY}` } })).json();
  for (const item of page.items) console.log(item.type, item.path);
  cursor = page.next_cursor;
} while (cursor);
```

Python:

```python
headers = {"Authorization": f"Bearer {os.environ['ENDORR_KEY']}"}
params = {"path": "/Invoices"}
while True:
    page = requests.get("https://auth.endorr.com/v1/cloud/folders", params=params, headers=headers).json()
    for item in page["items"]:
        print(item["type"], item["path"])
    if not page["next_cursor"]:
        break
    params["cursor"] = page["next_cursor"]
```

Up to 100 items per page (`limit` up to 500). When `next_cursor` is not null, pass it as `cursor` for the next page.

## Create folders

`POST /v1/cloud/folders` with `{ "path": "/Clients/Acme/2026" }` makes every missing folder on the way. Calling it again is safe: it answers `200` with the existing folder and `"created": false` instead of an error.

## Rename, move, delete

`PATCH /v1/cloud/files/{id}` or `PATCH /v1/cloud/folders/{id}` with a new `name`, a `move_to` path, or both. The destination folder is created when missing.

```bash
curl -X PATCH -H "Authorization: Bearer $ENDORR_KEY" -H "Content-Type: application/json" \
  -d '{"name": "invoice-paid.pdf", "move_to": "/Invoices/Paid"}' \
  "https://auth.endorr.com/v1/cloud/files/file_01JB8Q..."
```

`DELETE /v1/cloud/files/{id}` and `DELETE /v1/cloud/folders/{id}` move things to the trash. You can restore them in Endorr Studio for 28 days.

## Share links

`POST /v1/cloud/files/{id}/share` (or `/folders/{id}/share`) makes a view-only link anyone can open, for 7 days unless you pass `expires_in_days` (1 to 365). Add `max_downloads` to cap downloads.

```bash
curl -X POST -H "Authorization: Bearer $ENDORR_KEY" -H "Content-Type: application/json" \
  -d '{"expires_in_days": 30}' "https://auth.endorr.com/v1/cloud/files/file_01JB8Q.../share"

# { "share": { "id": "shr_…", "url": "https://…/s/…", "expires_at": "…" } }
```

## Search

`GET /v1/cloud/search?q=invoice` finds files and folders whose name contains the text, newest first, with their paths.

## Large files

For files over 100 MB (up to 50 GB), upload in three steps so the bytes go straight to storage:

1. `POST /v1/cloud/uploads` with `{ "path", "size_bytes", "content_type" }`.
2. `PUT` the bytes to `upload.url` with the headers in `upload.headers`. No API key on this request.
3. `POST` the `complete_url` from step 1. The file then appears in its folder.

```bash
START=$(curl -s -X POST -H "Authorization: Bearer $ENDORR_KEY" -H "Content-Type: application/json" \
  -d "{\"path\": \"/Videos/launch.mp4\", \"size_bytes\": $(stat -f%z launch.mp4)}" "https://auth.endorr.com/v1/cloud/uploads")

curl -T launch.mp4 "$(echo "$START" | jq -r .upload.url)"

curl -X POST -H "Authorization: Bearer $ENDORR_KEY" "https://auth.endorr.com$(echo "$START" | jq -r .complete_url)"
```

Large uploads support `on_conflict=rename` and `fail`. To replace a large file, delete the old one first.

## Errors

Every error has the same shape, with a stable `code` to branch on, a `message` written for people, and a `docs_url` pointing here:

```json
{
  "error": {
    "code": "forbidden",
    "message": "This API key lacks the \"files.write\" permission. Add it in Endorr Studio under Settings, API keys.",
    "request_id": "6f1c…",
    "docs_url": "https://endorr.com/developers#errors"
  }
}
```

| Status | code | What to do |
| --- | --- | --- |
| 401 | `unauthorized` | The key is missing, wrong, expired or revoked. |
| 402 | `storage_quota_exceeded` | The workspace is out of storage. `details` says how much is used and free. |
| 403 | `forbidden` | The key lacks a permission, or you cannot edit that folder. |
| 404 | `not_found` | Nothing at that path or id that this key can see. |
| 409 | `already_exists` | A file with that name is there and you passed `on_conflict=fail`. |
| 413 | `payload_too_large` | Over 100 MB: use a large upload. |
| 422 | `validation_failed` | A parameter is missing or wrong; `details` says which. |
| 429 | `rate_limited` | Too many requests. Wait a few seconds and retry. |

Mention the `request_id` when you contact support.

## Limits

- 600 requests per minute per key (an MCP call counts as one).
- 100 MB per one-request upload; 50 GB per large upload.
- 25 active keys per person.
- Storage counts against your plan, the same as files you add in Studio.
- End-to-end encrypted vault folders from the desktop app cannot be read or written through the API.

## Endpoint reference

All paths are relative to `https://auth.endorr.com`. The same list, with request and response schemas, is in the [OpenAPI document](https://auth.endorr.com/v1/cloud/openapi.json).

| Endpoint | Does | Needs |
| --- | --- | --- |
| `GET /v1/cloud` | Discovery: docs, OpenAPI and MCP links | nothing |
| `GET /v1/cloud/openapi.json` | OpenAPI 3.1 document | nothing |
| `POST /v1/cloud/mcp` | MCP server for AI agents | per tool |
| `GET /v1/cloud/me` | Who the key acts for, its permissions, storage | `files.read` |
| `GET /v1/cloud/folders?path=` | List a folder by path | `files.read` |
| `GET /v1/cloud/folders/{id}` | List a folder by id (root = /) | `files.read` |
| `POST /v1/cloud/folders` | Create a folder and missing parents | `files.write` |
| `PATCH /v1/cloud/folders/{id}` | Rename or move a folder | `files.write` |
| `DELETE /v1/cloud/folders/{id}` | Move a folder to the trash | `files.delete` |
| `POST /v1/cloud/folders/{id}/share` | Share link to a folder | `shares.create` |
| `PUT /v1/cloud/files?path=` | Upload in one request | `files.write` |
| `GET /v1/cloud/files?path=` | File details by path | `files.read` |
| `GET /v1/cloud/files/{id}` | File details by id | `files.read` |
| `GET /v1/cloud/files/download?path=` | Download by path (302) | `files.read` |
| `GET /v1/cloud/files/{id}/download` | Download by id (302) | `files.read` |
| `PATCH /v1/cloud/files/{id}` | Rename or move a file | `files.write` |
| `DELETE /v1/cloud/files/{id}` | Move a file to the trash | `files.delete` |
| `POST /v1/cloud/files/{id}/share` | Share link to a file | `shares.create` |
| `POST /v1/cloud/uploads` | Start a large upload | `files.write` |
| `POST /v1/cloud/uploads/{id}/complete` | Finish a large upload | `files.write` |
| `GET /v1/cloud/search?q=` | Search by name | `files.read` |

Questions or something missing? Ask us through the [Help center](https://endorr.com/help).
