# sharefiles.eu REST API

The sharefiles.eu REST API lets you upload, download, and manage files
programmatically. Files are stored as binary blobs and always served as raw
bytes — the API never returns base64 or data URLs.

> There is also an S3-compatible API at `/s3/` (AWS4-HMAC-SHA256, S3 XML). This
> document covers the native REST API only.

## Base URL

```
https://sharefiles.eu
```

All paths below are relative to this base URL. When self-hosting, substitute
your own origin.

## Authentication

Most programmatic endpoints authenticate with an **API key** passed as a Bearer
token:

```
Authorization: Bearer sk_your_api_key_here
```

- API keys always start with `sk_`.
- Create and revoke keys in the dashboard under **Settings → API Keys**.
- The key is shown only once at creation time — store it securely.
- File download endpoints (`/api/files/...`, `/api/download/...`) accept **either**
  an API key **or** a logged-in browser session.
- Upload via the browser endpoint (`/api/upload`) requires a session; for
  programmatic uploads use `/api/cli/upload` with an API key.

### Permissions

Each key carries one or more permission scopes:

| Scope            | Grants                                             |
| ---------------- | -------------------------------------------------- |
| `full`           | Everything                                         |
| `read`           | Read/download any file                             |
| `write`          | Upload/create in any directory                     |
| `read:<bucket>`  | Read only within the named bucket (directory)      |
| `write:<bucket>` | Write only within the named bucket (directory)     |
| `replicate`      | Node-to-node replication endpoints                 |

A bucket is the same thing as a top-level directory.

### Rate limiting

Each API key is limited to **1000 requests per hour**. Exceeding the limit
returns `429 Too Many Requests`:

```json
{ "error": "Rate limit exceeded (1000 requests per hour)" }
```

### Errors

Authentication failures return `401`:

```json
{ "error": "Invalid or missing API key" }
```

Errors are returned as JSON with an `error` field. Binary endpoints return the
appropriate HTTP status with no body on failure (`401`, `404`).

---

## Files

### Upload files

```
POST /api/cli/upload
Authorization: Bearer sk_...
Content-Type: multipart/form-data
```

Upload one or more files in a single request. This is the endpoint the
`sharefiles` CLI uses.

**Form fields**

| Field  | Type     | Required | Description                                                  |
| ------ | -------- | -------- | ------------------------------------------------------------ |
| `files`| File[]   | yes      | One or more files (repeat the field for multiple files)      |
| `dir`  | string   | no       | Target directory name (default `uploads`). Created if absent |

The directory name must match `^[a-zA-Z0-9._-]+$`. If the directory has
encryption enabled in your account, files are encrypted automatically before
storage.

**Response** `200 OK`

```json
{
  "files": [
    {
      "name": "screenshot.png",
      "directUrl": "https://sharefiles.eu/direct/abc123xyz",
      "size": 145238
    }
  ]
}
```

Each `directUrl` is a shareable download link valid for **1 hour**.

**Example**

```bash
curl -X POST https://sharefiles.eu/api/cli/upload \
  -H "Authorization: Bearer sk_your_api_key_here" \
  -F "dir=my-project" \
  -F "files=@report.pdf" \
  -F "files=@diagram.png"
```

### Download a file

```
GET /api/files/{fileId}
Authorization: Bearer sk_...   (or a browser session)
```

Returns the raw file bytes with the correct `Content-Type`.

**Query parameters**

| Param      | Description                                                    |
| ---------- | ------------------------------------------------------------- |
| `download` | `1` forces a download (attachment); `0`/omitted serves inline |

**Response headers**

| Header               | Description                                  |
| -------------------- | -------------------------------------------- |
| `Content-Type`       | The file's MIME type                         |
| `Content-Length`     | File size in bytes                           |
| `Content-Disposition`| `inline` or `attachment`                     |
| `X-Replica-Count`    | Confirmed remote replicas                    |
| `X-Required-Replicas`| Replicas required by policy                  |
| `X-Replica-Satisfied`| `true` when replication policy is satisfied  |

Encrypted files are served as-is (`application/octet-stream`); decrypt
client-side with your private key.

**Example**

```bash
curl -L -H "Authorization: Bearer sk_your_api_key_here" \
  "https://sharefiles.eu/api/files/FILE_ID?download=1" -o file.bin
```

### List file versions

```
GET /api/files/{fileId}/versions
Authorization: Bearer sk_...   (or a browser session)
```

Every upload to the same path creates a new version. Pass any version's ID to
retrieve the full history (newest first).

**Response** `200 OK`

```json
{
  "fileId": "FILE_ID",
  "versionGroupId": "GROUP_ID",
  "versions": [
    {
      "id": "VERSION_ID",
      "version": 2,
      "originalName": "report.pdf",
      "mimeType": "application/pdf",
      "fileSize": 204812,
      "isEncrypted": false,
      "isCurrent": true,
      "fileHash": "sha256...",
      "url": "/api/files/VERSION_ID",
      "createdAt": "2026-06-07T00:00:00.000Z"
    }
  ]
}
```

### Download a directory as an archive

```
GET /api/download/{dirId}
Authorization: Bearer sk_...   (or a browser session)
```

Downloads a directory (and its subdirectories) as a gzipped tar archive. Use
the literal `root` as `dirId` to archive files at the account root.

**Response** `200 OK` — `application/x-gzip`, `Content-Disposition: attachment; filename="<dir>.tgz"`

---

## Share links

Shareable, unauthenticated download URLs for a file or directory you own. Each
link can be **single-use**, **time-limited**, and/or **password-protected**.

A link's protections (single-use, expiry, **password**) are all set **when you
create it** via `POST /api/share-links` — there is no separate "set password"
call. To password-protect a link, include a `password` in the create body; the
recipient then supplies it when downloading (see *Use a share link* below).

### Create a share link

```
POST /api/share-links
Authorization: Bearer sk_...
Content-Type: application/json
```

**Body**

| Field              | Type    | Notes                                                        |
| ------------------ | ------- | ------------------------------------------------------------ |
| `fileId`           | string  | Share a file (provide this **or** `directoryId`)             |
| `directoryId`      | string  | Share a directory (served as a `.tgz`)                       |
| `singleUse`        | boolean | If true, the link works for exactly one download             |
| `maxRequests`      | number  | Explicit use cap (overrides `singleUse`); omit for unlimited |
| `expiresInMinutes` | number  | Time limit; omit for no expiry                               |
| `password`         | string  | Require this password to download                            |
| `label`            | string  | Optional human-readable label                                |

**Quick presets** (used by the apps' one-click "Quick link" buttons): time limits
`expiresInMinutes` = `60` (1 hour) / `480` (8 hours) / `1440` (24 hours), or request
caps `maxRequests` = `1` / `3` / `10`. Mix and match as you like.

**Response** `200 OK`

```json
{
  "token": "abc123…",
  "url": "https://sharefiles.eu/direct/abc123…",
  "expiresAt": "2026-06-08T00:00:00.000Z",
  "singleUse": true,
  "passwordProtected": true
}
```

**Example**

```bash
curl -X POST https://sharefiles.eu/api/share-links \
  -H "Authorization: Bearer sk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"fileId":"FILE_ID","singleUse":true,"expiresInMinutes":60,"password":"hunter2"}'
```

### Use a share link

```
GET /direct/{token}
```

No authentication. Returns the raw file bytes (or a `.tgz` for a directory).

- **Expired** → `410 Link expired`
- **Use limit reached** → `410 Link exhausted`
- **Password-protected**: supply the password as `?password=...` or the
  `X-Share-Password` header. A browser with no password is shown an unlock form;
  a wrong password returns `401`. The password check happens **before** a use is
  counted, so a wrong attempt never burns a single-use link.

```bash
curl -L "https://sharefiles.eu/direct/abc123?password=hunter2" -o file.bin
# or:
curl -L -H "X-Share-Password: hunter2" https://sharefiles.eu/direct/abc123 -o file.bin
```

---

## Offline sync

Flag files/directories for offline use and mirror them to local storage. The flag
is **per user** (a collaborator can sync a shared item independently of the owner).
There is also a drop-in client — see *Sync SDK* below.

### Manifest

```
GET /api/sync/manifest
Authorization: Bearer sk_...
```

Returns everything you've flagged for sync, expanded (directories recurse to all
descendants), with each file's current id/hash/version so a client can diff.

```json
{
  "directories": [
    { "id": "DIR", "name": "project", "parentId": null, "ownerId": "U", "permission": "write", "encryptionEnabled": false }
  ],
  "files": [
    { "versionGroupId": "G", "currentFileId": "F2", "version": 2, "fileHash": "sha256…",
      "fileSize": 2048, "mimeType": "text/plain", "originalName": "notes.txt",
      "directoryId": "DIR", "isEncrypted": false, "encryptionKeyId": null,
      "ownerId": "U", "permission": "write" }
  ],
  "generatedAt": "2026-06-07T00:00:00.000Z"
}
```

Send your device id as the **`X-Device-Id`** header (see *Devices* below). The
manifest is then filtered to what's targeted at that device; without it you get
only `all`-scoped items.

### Subscriptions (set the flag via API)

```
GET  /api/sync/subscriptions          → { "subscriptions": [ { resourceType, resourceId } ] }
POST /api/sync/subscriptions          → { "success": true, "enabled": true }
Authorization: Bearer sk_...
```

POST body: `{ "resourceType": "file"|"directory", "resourceId": "...", "enabled": true,
"scope": "all"|"selected", "deviceIds": ["..."] }`. `enabled`/`scope` default to
`true`/`all`; `deviceIds` apply only when `scope` is `selected`. For files,
`resourceId` is the file's `versionGroupId`.

## Devices

Native clients (desktop, mobile, CLI) register as **devices** so folders can sync
to all of them or a chosen subset.

```
POST   /api/devices            { "name": "My laptop", "platform": "desktop"|"mobile"|"cli"|"web" } → { "id" }
GET    /api/devices            → { "devices": [ { id, name, platform, lastSeenAt, createdAt } ] }
DELETE /api/devices/{id}       → { "success": true }   (revoke)
Authorization: Bearer sk_...
```

Registration also accepts an optional `syncRoot` (the device's local sync folder,
default `~/sharefiles`) used to build clipboard paths when sending files to that machine.
Update it later with `PATCH /api/devices/{id}` `{ name?, syncRoot? }`. Manage devices
in the dashboard under **Settings → Devices**.

## Send a file to a target

Push a file into sharefiles, choosing where it lands. Powers the desktop drag-to-send and
the mobile "Share with…" flows.

```
POST /api/send
Authorization: Bearer sk_...
Content-Type: multipart/form-data
```

| Field             | Notes                                                                 |
| ----------------- | --------------------------------------------------------------------- |
| `file`            | the file (required)                                                    |
| `target`          | `machine` \| `inbox` \| `directory`                                    |
| `deviceId`        | target machine (when `target=machine`)                                |
| `directoryId`     | target directory (when `target=directory`)                            |
| `clientEncrypted` | `true` if the bytes are already client-encrypted (encrypted dirs)     |
| `encryptionKeyId` | the key used, when `clientEncrypted`                                   |

- **machine** — uploads and targets sync to **only that device**; the file lands in the
  device's sync root. Response includes `clipboardPath` (e.g. `~/sharefiles/report.pdf`).
- **inbox** — drops into your **Inbox** (online-only, never synced).
- **directory** — a normal upload into that directory (follows its sync settings).

```bash
curl -X POST https://sharefiles.eu/api/send \
  -H "Authorization: Bearer sk_your_api_key_here" \
  -F "target=machine" -F "deviceId=DEVICE_ID" -F "file=@report.pdf"
# → { "ok": true, "fileId": "...", "versionGroupId": "...", "clipboardPath": "~/sharefiles/report.pdf" }
```

To list possible targets: `GET /api/devices` (machines) and `GET /api/directories`
(`{ directories: [{ id, name, parentId, isInbox }] }`). The **Inbox** is a per-user,
online-only directory shown in the dashboard under **Inbox**; files there can be moved
into directories. It cannot be flagged for offline sync.

### Pulling & pushing bytes

Download a synced file with `GET /api/files/{currentFileId}`. Push a local change
back with `POST /api/upload` (it accepts an **API key** too) — same as a normal
upload; a same-name upload becomes a new version. Encryption is client-side: send
`clientEncrypted=true` and `encryptionKeyId` when the folder is encrypted.

### Sync SDK

A dependency-free client is served at **`/sync/lib.js`** (works in browsers and
Node). It auto-detects available storage (Node FS, OPFS, or IndexedDB) and mirrors
everything you've flagged for sync:

```js
import { ShareFiles, hasFS } from 'https://sharefiles.eu/sync/lib.js';
const x = new ShareFiles({ key: 'sk_your_api_key' });
if (hasFS()) x.setLocation('./files');   // Node: a folder; browser: an OPFS subdir
await x.sync();                           // pulls flagged files down, pushes local changes up
```

Full SDK reference: [`/docs/llm/synchelper.md`](./synchelper.md).

---

## Account & stats

### Health check

```
GET /api/v1/health
Authorization: Bearer sk_...
```

```json
{
  "status": "healthy",
  "timestamp": "2026-06-07T00:00:00.000Z",
  "uptime": 3600,
  "version": "1.0.0",
  "message": "API is running successfully"
}
```

### Usage statistics

```
GET /api/v1/stats
Authorization: Bearer sk_...
```

Returns account info plus API usage counters.

```json
{
  "user": {
    "id": "USER_ID",
    "email": "you@example.com",
    "name": "Your Name",
    "role": "user",
    "createdAt": "2026-01-01T00:00:00.000Z"
  },
  "apiStats": {
    "totalApiKeys": 3,
    "requestsToday": 42,
    "requestsThisWeek": 310,
    "requestsThisMonth": 1280,
    "errorRate": "1.20%",
    "errorCount": 15
  },
  "meta": {
    "timestamp": "2026-06-07T00:00:00.000Z",
    "apiKey": "key name"
  }
}
```

### List users

```
GET /api/v1/users?limit=10&offset=0
Authorization: Bearer sk_...
```

Admins receive a paginated list of all users; regular users receive only their
own record.

| Param    | Default | Notes                |
| -------- | ------- | -------------------- |
| `limit`  | 10      | Max 100              |
| `offset` | 0       | Pagination offset    |

```json
{
  "users": [
    {
      "id": "USER_ID",
      "email": "you@example.com",
      "name": "Your Name",
      "role": "user",
      "emailVerified": "2026-01-01T00:00:00.000Z",
      "createdAt": "2026-01-01T00:00:00.000Z"
    }
  ],
  "meta": { "limit": 10, "offset": 0, "total": 1, "apiKey": "key name" }
}
```

---

## CLI authentication flow

These endpoints let an external tool obtain an API key through a browser
authorization step (used by the `sharefiles` CLI).

1. **Start** — `POST /api/cli/auth/init` → `{ token, url, expiresIn }`.
   Open `url` in a browser for the user to authorize (token valid 10 minutes).
2. **Poll** — `GET /api/cli/auth/poll?token=...` →
   `{ "status": "pending" }`, `{ "status": "expired" }`, or
   `{ "status": "complete", "apiKey": "sk_..." }` (returned once).
3. **Complete** — the browser page calls `POST /api/cli/auth/complete`
   (session-authenticated) with `{ token }`, which mints the key.

---

## Machine-readable spec

A full OpenAPI 3.1 description is available (no auth required):

```
GET /api/openapi.json
```

---

## Notes for self-hosters

The following endpoints exist for operating a sharefiles.eu cluster and are not
part of the user-facing API: `/api/replication/*` (node replication, requires
`replicate` permission), `/api/sync` (database sync, shared-secret auth), and
`/api/webhooks/stripe` (billing). See `docs/REPLICATION.md` for details.
