Documentation Menu

Delivery API

6 min read

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> takes locale (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) and preview (a preview token, to read the draft instead).
  • list takes prefix (a slash-terminated folder, such as docs/, 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), at and preview.
  • search takes q (required, 1 to 200 characters; "a phrase" and -excluded both work), locale, prefix, tags, limit (1 to 50, 10 by default) and offset (with offset + limit at most 200).
  • assets/<id> takes w (one of the instance's configured widths) and f (webp, avif, jpeg or png) for a scaled raster image. A private file's url already carries exp and sig, 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 preview or at on 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.
  • preview and at together 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 signed url that expires.
  • Each delivery key has a request budget, and so does each address before a key is checked. The defaults are on Configuration.

Last updated