Site API cookbook (v2)¶
This is a practical guide for common tasks with the Site API.
It’s written to be copy/paste friendly.
For higher-level tools, see the Sites CLI and Sites SDK. They can help with common publishing workflows without requiring you to build each request yourself.
Before you start¶
You need:
- Your site base URL:
https://<domain>/<space>/<site>/ - An API token with access to the site
The Site API base typically is:
https://<domain>/<space>/<site>/s/api/v2/
The regular Site API needs credentials: an HTTP Access Token, the site master
token, or — when calling from a browser — a site administrator session.
On a public site, anonymous file requests can use the public files endpoint
instead: https://<domain>/<space>/<site>/s/api/v2/pub/files/. The public
endpoint implements read requests only — GET, HEAD, and CORS OPTIONS.
Other methods there answer 404 Not Found: the endpoint has no handlers
for them. For anonymous read requests, use /pub/files/
instead of /files/ in the examples below.
Understanding Sites URLs shows where these paths sit among the other Site URL types.
The Hub shows the complete OpenAPI specification in the site's Overview. This cookbook covers common operations; use the specification for the full request and response details.
Authentication¶
Send a Site API token on requests to the regular endpoint:
Health¶
List a directory¶
A GET on a directory path returns its JSON listing by default; the flags
below filter or extend the result. (list=true matters when the path is a
file: it returns the file's metadata as JSON instead of the file itself.)
List the root directory:
List recursively:
curl -H "Authorization: Bearer ${SITE_TOKEN}" \
"https://<domain>/<space>/<site>/s/api/v2/files/?recursive=true"
A recursive listing shows a symbolic link to a directory as an entry, but does not list the files below it. To get these files, list the linked directory directly. Recursive archives and pattern deletes use the same rule.
Filter by glob pattern:
curl -H "Authorization: Bearer ${SITE_TOKEN}" \
"https://<domain>/<space>/<site>/s/api/v2/files/?match=*.pdf"
Filter by type:
curl -H "Authorization: Bearer ${SITE_TOKEN}" \
"https://<domain>/<space>/<site>/s/api/v2/files/?type=f"
Pagination (large listings)¶
Listings are paginated at 100 entries by default; limit accepts up to
500, and out-of-range values silently fall back to the default.
Request first page:
curl -H "Authorization: Bearer ${SITE_TOKEN}" \
"https://<domain>/<space>/<site>/s/api/v2/files/?limit=100"
If the response contains a non-empty continuation_token, request the next
page (the token and its cached page expire after about 3 minutes):
curl -H "Authorization: Bearer ${SITE_TOKEN}" \
"https://<domain>/<space>/<site>/s/api/v2/files/?limit=100&continuation_token=<TOKEN_FROM_PREVIOUS_RESPONSE>"
Download a file¶
curl -H "Authorization: Bearer ${SITE_TOKEN}" \
-o local-filename.ext \
"https://<domain>/<space>/<site>/s/api/v2/files/path/to/filename.ext"
Downloads carry a weak ETag built from size and modification time. To
verify content exactly, ask for a SHA-256 digest and compare it:
curl -H "Authorization: Bearer ${SITE_TOKEN}" \
-H "Want-Repr-Digest: sha-256" \
-D headers.txt \
-o local-file.bin \
"https://<domain>/<space>/<site>/s/api/v2/files/path/to/file.bin"
grep -i '^Repr-Digest:' headers.txt # Repr-Digest: sha-256=:<base64>:
The Repr-Digest header follows RFC 9530 and appears only when you ask for
it. Zero-byte files and archive responses carry no digest.
Upload a file (PUT)¶
Uploads use PUT and need write-capable credentials — the master token or a
read/write HTTP Access Token. Parent directories are created automatically.
A new file answers 201 Created; replacing an existing one answers
200 OK.
Example:
curl -H "Authorization: Bearer ${SITE_TOKEN}" \
-T ./local-file.txt \
"https://<domain>/<space>/<site>/s/api/v2/files/local-file.txt?backup_if_present=1"
To keep a copy of an existing file before replacing it, add
backup_if_present=1. You can set rename_to=<name> to choose the backup
filename. See the Site API spec for the exact rules.
Upload with a form (multipart)¶
Send one or more files as multipart/form-data parts named file. The API
stores every file part; a repeated name keeps the last one.
curl -X PUT -H "Authorization: Bearer ${SITE_TOKEN}" \
-F "file=@report.pdf" \
"https://<domain>/<space>/<site>/s/api/v2/files/reports/report.pdf"
Resumable (chunked) upload¶
For large or flaky transfers, upload in chunks with Content-Range. Ask the
API for the current size first, then send the next bytes:
curl -I -H "Authorization: Bearer ${SITE_TOKEN}" \
"https://<domain>/<space>/<site>/s/api/v2/files/big.iso"
# Upload-Offset: 1048576 (bytes already stored)
curl -X PUT -H "Authorization: Bearer ${SITE_TOKEN}" \
-H "Content-Range: bytes 1048576-2097151/4194304" \
--data-binary @part2.bin \
"https://<domain>/<space>/<site>/s/api/v2/files/big.iso"
If your offset is wrong, the API answers 409 Conflict with the
authoritative Upload-Offset header; resume from that number. The first
chunk restarts (truncates) the file. A slow storage volume can answer 503
with Retry-After: retry later, the already-received bytes are kept.
Create a directory¶
curl -X POST -H "Authorization: Bearer ${SITE_TOKEN}" \
"https://<domain>/<space>/<site>/s/api/v2/directories/reports/2026"
An existing directory also answers with success. PUT file uploads create
the parent directories of the target path automatically.
Delete a file or directory¶
Use DELETE to remove a file. If the path is a directory and you do not
provide a filter, the directory and its contents are removed.
curl -X DELETE -H "Authorization: ******" \
"https://<domain>/<space>/<site>/s/api/v2/files/path/to/file.txt"
Deletion is permanent. Check the path before sending the request. Use a token with write permission.
Bulk delete with a filter¶
DELETE with a match glob (and usually recursive=true) deletes every
entry the filter selects under the path:
curl -X DELETE -H "Authorization: ******" \
"https://<domain>/<space>/<site>/s/api/v2/files/tmp/?match=*.log&recursive=true"
- 200: every selected entry was removed.
- 207 Multi-Status: mixed result; the JSON body reports each entry.
- 404: nothing matched; 500: every entry failed.
- 202 Accepted: the request touches more than 500 entries or 100 MB; the API deletes in the background and you get no per-entry report. Failures appear only in the site logs.
Quick existence check (HEAD)¶
curl -I -H "Authorization: Bearer ${SITE_TOKEN}" \
"https://<domain>/<space>/<site>/s/api/v2/files/path/to/filename.ext"
This can be faster than downloading.
Download a folder as an archive¶
Zip:
curl -H "Authorization: Bearer ${SITE_TOKEN}" \
-o folder.zip \
"https://<domain>/<space>/<site>/s/api/v2/files/path/to/folder?archive=zip&recursive=true"
TGZ:
curl -H "Authorization: Bearer ${SITE_TOKEN}" \
-o folder.tgz \
"https://<domain>/<space>/<site>/s/api/v2/files/path/to/folder?archive=tgz&recursive=true"
archive=tar is also accepted (zip, tar, or tgz, case-insensitive).
Archives are created on the fly: the response has no ETag or digest, and
an empty selection answers 404 with a hint to add recursive=true.
Calling the API from a browser (CORS)¶
The Site API answers cross-origin preflight requests (OPTIONS) on its file
routes with 204 and, when an Origin is present, echoes it with
credentials allowed. Download responses for files and uploads expose the
progress headers browsers need (Upload-Offset, Content-Range, Range,
and the digest headers) through Access-Control-Expose-Headers.
For site content, the proxy adds CORS response headers only when you enable
CORS for the requesting domain: add the web app's domain to the site's
CORS extra domains in the Hub (see
Site settings). Authenticated API calls still need
credentials from the browser: an HTTP Access Token in the Authorization
header, or a site administrator session.
Important behaviour: .htpasswd can make paths look like they don’t exist¶
On public sites, the public Site API omits paths protected by a .htpasswd
file from listings and returns 404 for requests to those paths. Browser
and WebDAV requests to protected paths use HTTP Basic Authentication.
So:
- 404 can mean “doesn’t exist” OR “hidden on purpose”.
Error code quick guide¶
- 401: only on the password-prompt flows (WebDAV,
.htpasswd-protected browsing). Calls to the Site API with anAuthorizationheader answer 403 instead - 403: missing, invalid, or expired token; a browser session that is not a site administrator; a read-only token used for a write; or a write request sent to a public site's file paths
- 404: not found, hidden by
.htpasswd, or a method the endpoint doesn't implement (for example a write to the public files endpoint) - 405: a write blocked by platform read-only mode
- 409: the path exists but with the wrong type — for example asking to
list a file as a directory — or a resumable upload offset that does not
match the stored size (the response carries the actual
Upload-Offset) - 202: a bulk delete moved to the background; the work continues without a final per-entry report
- 507: storage quota exceeded — free up space or ask for a larger quota
- 503: the storage volume is not responding within the write timeout —
retry after the seconds named in
Retry-After - 5xx: server side issue; retry and check health