Guide
External API
Use scoped API tokens for scripts, CI/CD, and backends. Each token belongs to a service and can only touch that service's files under its key prefix.
Quick start
- Create a service — e.g. “Blog CDN” with prefix
blog/. - Create an API token for it with the access it needs.
- Copy the secret — it's shown once — and store it as
SA_TOKEN. - Check what the token can do:
curl -s -H "Authorization: Bearer $SA_TOKEN" "$SA_URL/api/me"Services & prefixes
A service is an app or API source. Every file and token belongs to exactly one. Each service has a default key prefix; uploads are stored under it so services never overwrite each other.
The effective prefixof a token is its own override if set, otherwise its service's prefix. If an upload key already starts with the prefix it isn't doubled: with prefix blog/, both hero.png and blog/hero.png are stored at blog/hero.png.
Authentication
Send the token in the Authorization header on every request:
Authorization: Bearer sa_xxxxxxxxxxxxxxxxTokens are stored as SHA-256 hashes and can't be recovered. Lost one? Rotate it from the dashboard to get a new secret without changing its permissions.
Permissions
| Permission | Allows |
|---|---|
| (any token) | GET /api/me, GET /api/services (own service) |
| files:list | GET /api/files |
| files:read | GET /api/files/<key> |
| files:write | POST /api/files, PUT /api/files/<key>, PATCH /api/files/<key> |
| files:delete | DELETE /api/files/<key> (also required to move a file) |
| stats:read | GET /api/stats (own token and service only) |
Token, service, and reconcile management stay dashboard-only.
Files
List
curl -s -H "Authorization: Bearer $SA_TOKEN" \
"$SA_URL/api/files?prefix=images/"Responses page with cursor; pass it back to get the next page.
Upload
curl -X POST -H "Authorization: Bearer $SA_TOKEN" \
-F "file=@./report.pdf" \
-F "key=documents/report.pdf" \
"$SA_URL/api/files"Replace contents
curl -X PUT -H "Authorization: Bearer $SA_TOKEN" \
-H "Content-Type: application/pdf" \
--data-binary @./report-v2.pdf \
"$SA_URL/api/files/blog/documents/report.pdf"Move or rename
curl -X PATCH -H "Authorization: Bearer $SA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"key": "blog/archive/report.pdf"}' \
"$SA_URL/api/files/blog/documents/report.pdf"Needs files:write and files:delete. Returns 409 if the destination exists — add "overwrite": true to replace it. Sessions can also send serviceId to relabel a file.
Delete
curl -X DELETE -H "Authorization: Bearer $SA_TOKEN" \
"$SA_URL/api/files/blog/documents/report.pdf"Statistics
GET /api/stats returns totals, percent shares by token, service, and route, a gap-filled time series, and the 25 most recent requests. Tokens only see their own traffic.
curl -s -H "Authorization: Bearer $SA_TOKEN" \
"$SA_URL/api/stats?from=2026-09-01T00:00:00Z&tz=Europe/Paris"Series buckets are hourly for windows up to 2 days, daily up to ~13 months, then monthly — in the tz time zone (default UTC).
Public URLs
Every object is readable without authentication at /files/<key>, with ETag, Last-Modified, and a one-hour cache. Add ?download to force a download.
Errors
Errors are JSON: { "error": "Human-readable message" }
- 400Invalid input or key
- 401Missing, invalid, or expired token or session
- 403Missing permission, key outside the token prefix, or file owned by another service
- 404Object, service, or token not found
- 409Conflict — destination exists, or service still has files/tokens
- 429Too many failed sign-in attempts
- 500Server, database, or R2 error
Managing tokens
- One token per integration, named after where it runs.
- Grant the smallest access preset that works; add an expiry for temporary access.
- Rotate on a schedule or after a leak — the old secret stops working instantly.
- Watch Last used and the Statistics page for unexpected activity.