curl --request GET \
--url https://app.sandywp.com/api/app/sites/{id}/fs/{op} \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.sandywp.com/api/app/sites/{id}/fs/{op}"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.sandywp.com/api/app/sites/{id}/fs/{op}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.sandywp.com/api/app/sites/{id}/fs/{op}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.sandywp.com/api/app/sites/{id}/fs/{op}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://app.sandywp.com/api/app/sites/{id}/fs/{op}")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.sandywp.com/api/app/sites/{id}/fs/{op}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{}{
"error": {
"code": "auth_required",
"message": "A logged-in user is required.",
"details": {}
}
}{
"error": {
"code": "capability_required",
"message": "Your role does not allow this.",
"details": {
"capability": "internals"
}
}
}{
"error": {
"code": "invalid_request",
"message": "<string>",
"details": {}
}
}{
"error": {
"code": "invalid_request",
"message": "<string>",
"details": {}
}
}File manager — read operations
One route handles every read-only file-manager operation; op selects it. This app authenticates the caller and proxies the request to the sandbox’s file bridge (including streamed downloads). The bridge does not load WordPress, so file operations keep working while a plugin or theme crashes the site. The response body for a given op is whatever the bridge returns, and is not independently validated here. A read of a file over 5 MB is refused with 413 (use download instead).
curl --request GET \
--url https://app.sandywp.com/api/app/sites/{id}/fs/{op} \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.sandywp.com/api/app/sites/{id}/fs/{op}"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.sandywp.com/api/app/sites/{id}/fs/{op}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.sandywp.com/api/app/sites/{id}/fs/{op}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.sandywp.com/api/app/sites/{id}/fs/{op}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://app.sandywp.com/api/app/sites/{id}/fs/{op}")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.sandywp.com/api/app/sites/{id}/fs/{op}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{}{
"error": {
"code": "auth_required",
"message": "A logged-in user is required.",
"details": {}
}
}{
"error": {
"code": "capability_required",
"message": "Your role does not allow this.",
"details": {
"capability": "internals"
}
}
}{
"error": {
"code": "invalid_request",
"message": "<string>",
"details": {}
}
}{
"error": {
"code": "invalid_request",
"message": "<string>",
"details": {}
}
}Authorizations
Personal API token, sent as Authorization: Bearer <SANDYWP_API_KEY>. Obtain one from the dashboard account menu (API keys), sandywp auth login, or POST /api/account/tokens. A missing or invalid token returns 401 auth_required. Scoped OAuth tokens are limited to the Imports family and GET /api/account/me; see the top-level conventions.
Headers
Selects which of the caller's workspaces to act in, for accounts belonging to more than one (legacy alias: X-SandyWP-Organization). Defaults to the caller's own personal workspace when omitted. Every id in this API (sandboxes, Templates, repositories) is scoped to a single workspace, so this header changes which set of resources is visible.
Path Parameters
The sandbox id.
list: one directory's entries (needs path).
tree: recursive directory tree (needs path).
read: a file's contents (needs path).
download: stream one file as an attachment (needs path).
zip: compress a folder and stream the .zip (needs path; up to 256 MB uncompressed or 20,000 files).
list, tree, read, download, zip Query Parameters
Base64url-encoded (RFC 4648 §5, unpadded) path relative to the WordPress root — for example the base64url encoding of wp-content/plugins, not the plain path itself. Names stay encoded end-to-end (including in this app's own proxying to the sandbox) so that sensitive filenames are never exposed in plaintext to an intermediate WAF. Required for list, tree, read, download, write, mkdir, and delete; omitted for read without a path defaults to the WordPress root.
Response
list/tree/read return JSON metadata (or file content); download and zip stream the file or archive with the sandbox-supplied content type.
The response is of type object.

