Delivery API
The delivery API under /d/v1 answers a site's reads of published content: one document, a listing, a search or a file.
Why the key your site holds can only read
A site needs to read content and do nothing else. So it gets a read-only API of its own, with a key that can do no harm if it leaks. Delivery explains that design and the cache behind it. This page is the reference a site's engineers work from. It lists every route, its parameters, the shape of each answer and the headers to cache by. The full contract, field by field, is in /openapi.json.
What the delivery API serves
The routes
Every route except the file route takes a delivery key over HTTP basic auth, and reads the one environment that key is bound to.
| Route | Answers |
|---|---|
GET /d/v1/<project>/<env>/content/<key> |
One document, resolved for a locale, references and files inlined |
GET /d/v1/<project>/<env>/list |
The folders and documents under a prefix, without bodies |
GET /d/v1/<project>/<env>/search |
The published sections that answer a question, best first |
GET /d/v1/<project>/assets/<id> |
A file's bytes, with no key for a public file |
Parameters
content/<key>takeslocale(the project's default when absent),refs(how many levels of references to inline, 0 to 5, 3 by default),at(an ISO 8601 instant, to read what was live then) andpreview(a preview token, to read the draft instead).listtakesprefix(a slash-terminated folder, such asdocs/, or absent for the whole environment),locale,tags(comma-separated; an item must carry all of them),depth(1 to 10 levels below the prefix),atandpreview.searchtakesq(required, 1 to 200 characters;"a phrase"and-excludedboth work),locale,prefix,tags,limit(1 to 50, 10 by default) andoffset(withoffset + limitat most 200).assets/<id>takesw(one of the instance's configured widths) andf(webp,avif,jpegorpng) for a scaled raster image. A private file'surlalready carriesexpandsig, so append&w=to it, never a second?.
A document and its _meta
A content answer is { key, locale, type, content, _meta }. content is the merged body: the requested locale's fields, with the default locale filling any gap. _meta says how the answer was made:
| Field | Meaning |
|---|---|
resolvedLocale |
The locale the body came from. |
fallbackLocale |
The locale that filled gaps, or null when nothing was filled. |
fallbacks |
The fields that came from the fallback. ["*"] means the whole document did. |
version, publishedAt |
The live version and when it went live. On a preview, publishedAt is null: that version has not gone live. |
contentHash |
A hash of the resolved body. |
refs, assets |
The documents and files that were inlined. |
refsTruncated, assetsTruncated |
Present and true when the caps below cut the answer short. |
preview |
Present and true when the answer is a draft. |
A list answer is { prefix, locale, folder, folders, items }. Here folder is the prefix's own published _index, and each item has its key, title, tags, version and publishedAt. A search answer is { hits, total, _meta }. Each hit names its key and title, the heading it was found under and that heading's anchor, a plain-text snippet, a score, and its locale. On a hit about the whole page, heading and anchor are both null. A hit served from the default locale carries fallbackLocale.
Caching headers
Every content, list and search answer carries a weak ETag, Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=60 and Vary: Authorization. Send the ETag back in If-None-Match, and if the answer has not changed you get a 304 with no body and the same headers. A read with ?preview= gets Cache-Control: no-store and no ETag, and it is never a 304. A read with ?at= bypasses the CMS's cache and also gets Cache-Control: no-store, so no CDN keeps it. It does keep its ETag, though, and can still answer a 304.
Refusals
A missing or unknown key gets 401 unauthorized. A CLI key, or a key for another environment, gets 403 forbidden. Three things get 404 not_found: a document with nothing published, a prefix without its closing slash, and search on a project that has turned search off. A folder with nothing in it is not an error; it is an empty listing. A bad parameter gets 422 validation_failed, with fields naming the parameter. Once a key has spent its budget, the answer is 429 rate_limited. A 503 means the rate-limit counter could not be reached, and a retry shortly afterwards will do.
How to
API
Read one document in another locale, and look at its _meta:
curl -s -u '<keyId>:<secret>' \
'http://localhost:3000/d/v1/<project>/<env>/content/<key>?locale=<locale>'When the document has no translation in that locale, _meta.fallbackLocale is the default locale and _meta.fallbacks is ["*"].
List what is under a folder, to two levels deep:
curl -s -u '<keyId>:<secret>' \
'http://localhost:3000/d/v1/<project>/<env>/list?prefix=<folder>/&depth=2'Search the same environment:
curl -s -u '<keyId>:<secret>' \
'http://localhost:3000/d/v1/<project>/<env>/search?q=<word>&limit=5'Link each hit to /<key>#<anchor> when anchor is set, and to /<key> when it is null. Delivery shows the round trip with the ETag and the 304.
SDK
The SDK sends the same requests, using the delivery key from QUILLET_KEY_ID and QUILLET_KEY_SECRET, and it revalidates with the ETag on every read:
import { quillet } from '@quilletcms/sdk/node';
const q = quillet();
const page = await q.get('docs/intro', { locale: 'de-DE' });
const nav = await q.list('docs/', { depth: 2 });
const answer = await q.search('webhook', { limit: 5 });A 404 arrives as a NotFoundError, a 401 as an UnauthorizedError, and a 429 as a RateLimitedError.
Limits
- Delivery only reads. It has no route that writes, and a delivery key reaches one environment of one project.
- Without a preview token, delivery serves published content only. A preview token opens one document in one locale, and in a listing the documents under one environment, for fifteen minutes.
- Search covers published content only, and
previeworaton a search is a 422. Search has no fuzzy matching and no prefix matching. It reaches the first 200 results and no further, and it finds text in a component under the component's key, not under the page that includes it. Search index says what is indexed. previewandattogether are a 422 on every route.- References are inlined five levels deep at most. One answer inlines at most 200 references and 500 files.
- A private file reaches a site only through a key with
assets:private, or through a signedurlthat expires. - Each delivery key has a request budget, and so does each address before a key is checked. The defaults are on Configuration.
Related
- Delivery: why delivery is a second API, and what its cache holds.
- Authentication: minting the delivery key.
- Locales and fallback: how
_meta.fallbacksis decided. - Reading content: the same reads through the SDK.
- Search: searching from a site.
- Previewing drafts: the preview token on your own site.
- Search: what the search route indexes, matches and ranks.