Skip to main content
Livry has two surfaces, and they are not alternatives — you use both.

Public API

https://api.livry.dev/public/v1 — REST, JSON, OAuth 2.0 bearer tokens. Create Themes, edit tokens, publish versions. Called from your backend or your CI, never from a browser.

Serving edge

https://cdn.livry.dev — the published files themselves. Called by your app at runtime, at high volume, over a CDN. No bearer token.
The Public API never returns tokens ready to render. It returns the authoring view — the DTCG document you are editing — and the URLs on the edge where the rendered result is served. Your app reads the edge; your tooling reads the API.

Base URL

The version is in the path and in the token audience, so a token minted for v1 cannot be replayed against a future v2.

The resource model

Nothing sits between an Environment and a Theme, and nothing sits below a Variant. A Theme with no Variants is not an error — it resolves to its own defaults unchanged. Environments are fully isolated. Nothing moves between them, nothing auto-promotes, and this API offers no operation that crosses one.
Everything is addressed by id, never by slug. A slug is renameable, and a machine client stores what it was handed. The slug is returned as an ordinary attribute, and ?slug= on a list endpoint is how you turn one into the other.

Response shapes

Every response has one of two outermost shapes.
The envelope exists so a response can grow a sibling field later — a deprecation notice, a warning list — without that being a breaking change to the resource’s own shape.

Paging

Pages are numbered, not continuation-token. Scripts drain collections and dashboards render “page 3 of 40”; neither can do anything with an opaque cursor. totalCount and totalPages are 0 unless you asked for them, not null. Counting is a second query, and paging a collection needs the count once rather than on every page. A page that has items cannot legitimately have a total of zero, so the pair is unambiguous. pageCount is always populated — it is the length of items, so you never have to length-check the array to know you have reached the last page.
pageNumber is capped at 1000 because a numbered page is implemented as a skip, and a deep skip re-scans. If you are draining a large collection, raise pageSize rather than walking past page 1000.

Conventions

  • Timestamps are ISO 8601, UTC — 2026-09-18T04:21:07Z.
  • Enums are camelCase names, never ordinals: "react", "chakraUI", "signed", "descending".
  • Responses are pretty-printed. They are small, and the reader is usually a person with a curl.
  • Unknown query parameters are ignored; a parameter that will not convert is a 400 naming it.

Concurrency

Token documents are edited through an optimistic-concurrency check. Read the token set, send its etag back with the write, and a 409 means somebody else got there first — re-read and retry.
The ETag is the draft blob’s, not the entity’s. The token document lives on Blob Storage, so a Cosmos ETag stopped saying anything about whether the tokens had changed. A null etag is legitimate and means “there is no draft yet” — which is what a first edit on a freshly created Theme sends.
It is a query parameter today, ?etag=…, not an If-Match header. That is a known wart: the header is the correct idiom and is where it will end up. See Errors.

What this API deliberately does not do

Adding any of these is a decision, not an oversight.