Skip to content

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:

curl -H "Authorization: Bearer ${SITE_TOKEN}" ...

Health

curl -H "Authorization: Bearer ${SITE_TOKEN}" \
  "https://<domain>/<space>/<site>/s/api/v2/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:

curl -H "Authorization: Bearer ${SITE_TOKEN}" \
  "https://<domain>/<space>/<site>/s/api/v2/files/"

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 an Authorization header 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