# Files

Browse, read, upload, rename and delete the files of a site.

## List a folder

`GET /sites/{site_id}/files` · scope `files:read`

Folders first, then files; up to 5,000 entries. Symbolic links are listed (type "link") but never followed. Available under the same rule as the File Manager: the site is installed, the plan is active and the site is not suspended.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `path` | query | string | no | Folder path relative to the site folder (default: the site folder itself). |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/sites/123/files?path=wp-content" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/sites/123/files?path=wp-content", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files?path=wp-content", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "path": "wp-content",
    "items": [
      {
        "name": "themes",
        "type": "folder",
        "size": null,
        "modified": "2026-09-01T10:00:00Z",
        "mode": "644",
        "path": "wp-content/themes"
      }
    ],
    "truncated": false
  }
}
```

Errors: `conflict`, `forbidden`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`

## File or folder details

`GET /sites/{site_id}/files/stat` · scope `files:read`

Type, size, modification time and mode of one path.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `path` | query | string | yes | Path relative to the site folder, e.g. "wp-content/uploads" ("" or "/" = the site folder). |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/sites/123/files/stat?path=index.php" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/sites/123/files/stat?path=index.php", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files/stat?path=index.php", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "name": "index.php",
    "type": "file",
    "size": 405,
    "modified": "2026-09-01T10:00:00Z",
    "mode": "644",
    "path": "index.php"
  }
}
```

Errors: `not_found`, `rate_limited`, `scope_missing`, `unauthorized`

## Read (download) a file

`GET /sites/{site_id}/files/content` · scope `files:read`

Returns a file of up to 5 MB (the limit is in GET /limits). Use encoding=raw to stream the bytes directly.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `path` | query | string | yes | Path relative to the site folder, e.g. "wp-content/uploads" ("" or "/" = the site folder). |
| `encoding` | query | string (one of: base64, text, raw) | no | base64 (JSON, any file), text (JSON, UTF-8 files) or raw (the bytes themselves, with a Content-Type). Default: `base64`. |

### Example

```bash
curl -s "https://app.pbn.ltd/api/v1/sites/123/files/content?path=robots.txt&encoding=text" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.get("https://app.pbn.ltd/api/v1/sites/123/files/content?path=robots.txt&encoding=text", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files/content?path=robots.txt&encoding=text", {
  method: "GET",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "path": "robots.txt",
    "size": 67,
    "modified": "2026-09-01T10:00:00Z",
    "content_type": "text/plain",
    "content": "User-agent: *\nDisallow:\n",
    "encoding": "text"
  }
}
```

Errors: `not_found`, `rate_limited`, `scope_missing`, `too_large`, `unauthorized`

## Upload (write) a file

`PUT /sites/{site_id}/files/content` · scope `files:write`

Writes a file of up to 25 MB atomically (a temporary file renamed into place), owned by the site's web server user. Refuses to write through symbolic links and into folders the platform manages.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `path` | query | string | yes | Where to write it, relative to the site folder. |
| `content` | body | string | no | Text content (UTF-8). Or use content_base64. |
| `content_base64` | body | string | no | The file, base64-encoded. Or send the raw bytes as the request body (Content-Type application/octet-stream) or a multipart form with a "file" field. |
| `overwrite` | query | boolean | no | Replace an existing file. Default: `False`. |
| `mkdirs` | query | boolean | no | Create missing folders. Default: `True`. |

### Example

```bash
curl -s -X PUT "https://app.pbn.ltd/api/v1/sites/123/files/content?path=hello.txt&overwrite=true" \
  -H "Authorization: Bearer $PBN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content": "Hello world\n"}'
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.put("https://app.pbn.ltd/api/v1/sites/123/files/content?path=hello.txt&overwrite=true", headers=headers, json={"content": "Hello world\n"}, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files/content?path=hello.txt&overwrite=true", {
  method: "PUT",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"},
  body: JSON.stringify({"content": "Hello world\n"})
});
console.log(res.status, await res.json());
```

### Response

`201`

```json
{
  "data": {
    "name": "hello.txt",
    "type": "file",
    "size": 12,
    "modified": "2026-09-01T10:00:00Z",
    "mode": "644",
    "path": "hello.txt",
    "written": 12
  }
}
```

Errors: `conflict`, `forbidden`, `not_found`, `rate_limited`, `scope_missing`, `too_large`, `unauthorized`

## Create a folder

`POST /sites/{site_id}/files/folders` · scope `files:write`

Creates a folder (and any missing parent folders).

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `path` | body | string | yes | The new folder path. |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/files/folders" \
  -H "Authorization: Bearer $PBN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"path": "wp-content/uploads/reports"}'
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/sites/123/files/folders", headers=headers, json={"path": "wp-content/uploads/reports"}, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files/folders", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"},
  body: JSON.stringify({"path": "wp-content/uploads/reports"})
});
console.log(res.status, await res.json());
```

### Response

`201`

```json
{
  "data": {
    "name": "new",
    "type": "folder",
    "size": null,
    "modified": "2026-09-01T10:00:00Z",
    "mode": "755",
    "path": "wp-content/new"
  }
}
```

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`

## Rename or move

`POST /sites/{site_id}/files/move` · scope `files:write`

Renames or moves a file or folder inside the site folder.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `path` | body | string | yes | What to move. |
| `to` | body | string | yes | The new path. |
| `overwrite` | body | boolean | no | Replace an existing file at the destination. Default: `False`. |

### Example

```bash
curl -s -X POST "https://app.pbn.ltd/api/v1/sites/123/files/move" \
  -H "Authorization: Bearer $PBN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"path": "hello.txt", "to": "old/hello.txt"}'
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.post("https://app.pbn.ltd/api/v1/sites/123/files/move", headers=headers, json={"path": "hello.txt", "to": "old/hello.txt"}, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files/move", {
  method: "POST",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`, "Content-Type": "application/json"},
  body: JSON.stringify({"path": "hello.txt", "to": "old/hello.txt"})
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "name": "index.php",
    "type": "file",
    "size": 405,
    "modified": "2026-09-01T10:00:00Z",
    "mode": "644",
    "path": "index.php",
    "moved_from": "old.php"
  }
}
```

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`

## Delete a file or folder

`DELETE /sites/{site_id}/files` · scope `files:write` · **destructive**

Deletes a file, or a folder (with recursive=true when it is not empty). There is no undo - take a backup first for anything important.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `site_id` | path | integer | yes | The site id (see GET /sites). |
| `path` | query | string | yes | Path relative to the site folder, e.g. "wp-content/uploads" ("" or "/" = the site folder). |
| `recursive` | query | boolean | no | Required to delete a folder that is not empty. Default: `False`. |

### Example

```bash
curl -s -X DELETE "https://app.pbn.ltd/api/v1/sites/123/files?path=hello.txt" \
  -H "Authorization: Bearer $PBN_API_KEY"
```

```python
import os
import requests

headers = {"Authorization": "Bearer " + os.environ["PBN_API_KEY"]}
r = requests.delete("https://app.pbn.ltd/api/v1/sites/123/files?path=hello.txt", headers=headers, timeout=120)
print(r.status_code, r.json())
```

```javascript
const res = await fetch("https://app.pbn.ltd/api/v1/sites/123/files?path=hello.txt", {
  method: "DELETE",
  headers: {Authorization: `Bearer ${process.env.PBN_API_KEY}`}
});
console.log(res.status, await res.json());
```

### Response

`200`

```json
{
  "data": {
    "path": "old.zip",
    "deleted": true,
    "entries_removed": 1
  }
}
```

Errors: `conflict`, `not_found`, `rate_limited`, `scope_missing`, `unauthorized`
