Skip to content

HTTP API

Everything under /_/api/v1, authenticated with a bearer token. A token is not a cookie: a cross-site request cannot borrow one, so these routes carry no CSRF protection and the browser UI's cookie does not work here. Two doors, one lock each.

Authorization: Bearer cdn_<key id>_<secret>

Objects

List

GET /_/api/v1/zones/{zone}/objects
parameter default
prefix none only paths starting with it
delimiter none / folds the listing into folders
cursor none continue from next_cursor of the previous page
limit 100 entries per page, at most 1000

Paging is by path rather than by offset. An object inserted while a client pages through would shift every later row under an offset, so a sync would silently skip one; a path cursor cannot.

With delimiter=/ the answer is a folder view: objects directly under the prefix, plus the folders below it. Both fill the same page and share the same cursor, so limit bounds objects and prefixes together and each entry is named exactly once across the pages. Treat next_cursor as opaque, since it may name either, and stop when it comes back null. Needs the read scope.

Upload or replace

PUT /_/api/v1/zones/{zone}/objects/{path}

The body is the bytes. 201 when the object is new, 200 when it replaced one.

header
Content-Type stored and returned; guessed from the path when absent
X-CDN-Cache-Control overrides the zone's caching header for this object

Refused with 413 past CDN_MAX_UPLOAD_BYTES and 507 past a storage quota, in both cases before the body is transferred when the client declares a length. Needs write, and the zone must be a storage zone.

Describe

GET /_/api/v1/zones/{zone}/objects/{path}

The body carries size, etag, content type, timestamps and the public URL. Needs read.

Delete

DELETE /_/api/v1/zones/{zone}/objects/{path}

204, and 204 again for a path that is not there: a delete has to be safe to retry, and a client that timed out cannot know whether the first attempt landed. Needs delete.

Zones

Sign a URL

POST /_/api/v1/zones/{zone}/sign/{path}

{"expires_in": 3600}, in seconds. Returns a URL carrying an expiry and a signature. Needs read. See Private links.

Purge

POST /_/api/v1/zones/{zone}/purge

One of three bodies:

{"paths": ["img/logo.png", "img/icon.png"]}
{"prefix": "img/"}
{"all": true}

Answers {"purged": n}. Nothing is deleted: a storage zone's objects stay in S3 and a pull zone's stay at the origin. Needs purge.

For a token with a path prefix, {"all": true} means everything that token can reach.

Statistics

GET /_/api/v1/zones/{zone}/stats?days=30

The body carries totals, a daily series, the busiest paths and a breakdown by outcome. Needs stats.

Status codes

400 a request the client can fix; the message says how
401 no credential, or one that is not ours
403 a credential without the scope, prefix or zone for this
404 no such zone or object. Also what another tenant's zone answers, because whether it exists is not something to learn from the difference between two codes
413 past CDN_MAX_UPLOAD_BYTES
429 writing faster than the token's allowance; carries Retry-After
507 past a storage quota

Health

GET /_/health          → {"status": "ok", "database": "ok"}
GET /_/health?deep=1   → also checks the object store

Unauthenticated. See Running it.