> ## Documentation Index
> Fetch the complete documentation index at: https://docs.textsetu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Content Delivery Manifest

> The manifest contract: exact shape, cache headers, and status codes.

The manifest is the document your app reads to find its translations. This page
is the contract: its exact shape, the cache headers, and the status codes.

For how to use it, see
[Using a distribution in your app](/docs/guides/content-delivery-integration).

***

## Endpoints

```
GET /api/v1/content/{publicKey}/manifest.json
GET /api/v1/content/{publicKey}/b/{blob}
```

Both are **public and unauthenticated** — the manifest URL is the credential, so
treat it as you would any unlisted URL. Both send
`Access-Control-Allow-Origin: *`, so they work from a browser app on any origin.

You never construct the blob URL yourself — take `files[].url` from the
manifest. It's absolute, so a distribution publishing to a customer-owned bucket
or CDN needs no client change.

Both appear in the generated [API Reference](/api/introduction) under **Content
delivery**.

***

## The manifest

```jsonc theme={null}
{
  "schema": "textsetu.distribution/v1",
  "distribution": { "key": "a1b2c3…", "name": "Mobile app", "channel": "stable" },
  "release": {
    "id": "…",
    "version": 42,
    "contentHash": "sha256:…",
    "publishedAt": "2026-08-14T10:11:12.000Z"
  },
  "project": { "id": "…", "sourceLanguage": "en" },
  "format": { "id": "json-nested", "keySeparator": ".", "extension": ".json" },
  "languages": ["en", "fr", "de"],
  "languageDetails": [
    { "code": "en", "label": "English", "icon": "🇬🇧", "direction": "ltr" },
    { "code": "fr", "label": "Français", "icon": "🇫🇷", "direction": "ltr" }
  ],
  "files": [
    {
      "language": "fr",
      "module": null,
      "path": "fr.json",
      "url": "https://cdn.example.com/a1b2c3…/b/9f2c….json",
      "contentHash": "sha256:…",
      "byteSize": 8123,
      "contentType": "application/json; charset=utf-8"
    }
  ],
  "minAppVersion": null,
  "maxAppVersion": null,
  "bundles": []
}
```

### Fields

| Field                             | Type                   | Notes                                                                           |
| --------------------------------- | ---------------------- | ------------------------------------------------------------------------------- |
| `schema`                          | string                 | Versioned identifier. Refuse an unknown **major** version rather than guessing. |
| `distribution.key`                | string                 | The public key in the URL.                                                      |
| `distribution.name`               | string                 | The name shown in TextSetu. Informational.                                      |
| `distribution.channel`            | string                 | `stable` or `prerelease`.                                                       |
| `release.id`                      | string                 | Opaque release identifier.                                                      |
| `release.version`                 | number                 | Increments per release. Also sent as `X-TextSetu-Release`.                      |
| `release.contentHash`             | string                 | `sha256:…` over the whole release. Changes whenever any file does.              |
| `release.publishedAt`             | string                 | ISO 8601 timestamp.                                                             |
| `project.id`                      | string                 | The project this came from.                                                     |
| `project.sourceLanguage`          | string \| null         | Use as your `fallbackLng`.                                                      |
| `format.id`                       | string                 | e.g. `json-nested`, `arb`, `xliff-2.0`.                                         |
| `format.keySeparator`             | string                 | The separator used in keys — also what module splitting splits on.              |
| `format.extension`                | string                 | File extension, including the dot.                                              |
| `languages`                       | string\[]              | Every language code this release publishes.                                     |
| `languageDetails`                 | object\[] \| undefined | **Optional.** Order-aligned with `languages`. See below.                        |
| `files`                           | object\[]              | One entry per published file. See below.                                        |
| `minAppVersion` / `maxAppVersion` | null                   | **Reserved.** Always `null` today.                                              |
| `bundles`                         | array                  | **Reserved.** Always `[]` today.                                                |

### `files[]`

| Field         | Type           | Notes                                                                                                                                                      |
| ------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `language`    | string         | Language code.                                                                                                                                             |
| `module`      | string \| null | The module, or `null` when the distribution isn't split by module. **Always present**, so you can key on `(language, module)` without branching on config. |
| `path`        | string         | Logical path, e.g. `fr.json` or `fr/checkout.json`.                                                                                                        |
| `url`         | string         | Absolute URL. Content-addressed and immutable.                                                                                                             |
| `contentHash` | string         | `sha256:…` of this file. A client already holding this hash can skip the download.                                                                         |
| `byteSize`    | number         | Size in bytes.                                                                                                                                             |
| `contentType` | string         | The `Content-Type` the file is served with.                                                                                                                |

### `languageDetails[]`

| Field       | Type           | Notes                          |
| ----------- | -------------- | ------------------------------ |
| `code`      | string         | Language code.                 |
| `label`     | string         | Display name, e.g. `Français`. |
| `icon`      | string \| null | Flag or icon character.        |
| `direction` | string         | `ltr` or `rtl`.                |

Always fall back — the field is optional and releases published before it
existed don't carry it:

```js theme={null}
const langs =
  manifest.languageDetails ??
  manifest.languages.map((c) => ({ code: c, label: c }));
```

### Shape notes

* **`files` is an array, not a map keyed by language.** A distribution split by
  module has several files per language, and keeping it an array made that an
  additive change rather than a breaking reshape.
* **The manifest is a bare JSON document**, deliberately *not* the
  `{success, data}` envelope the rest of `/api/v1` uses. The identical bytes are
  also written to object storage and read by clients that know nothing about
  TextSetu.

***

## Caching

| Resource | `Cache-Control`                                                        |
| -------- | ---------------------------------------------------------------------- |
| Manifest | `public, max-age=60, stale-while-revalidate=600, stale-if-error=86400` |
| Files    | `public, max-age=31536000, immutable`                                  |

Files are content-addressed: the URL contains the content hash, so it never
needs revalidating and a browser will not re-request it. Only the manifest is
invalidated on publish — one path per release.

`stale-if-error=86400` means **your app keeps working from the last good
manifest for a day if the origin is unreachable.** This is the main reason to
read the manifest rather than hardcoding file URLs.

### Conditional requests

Both endpoints support `ETag`. Send `If-None-Match` to get a `304`:

```js theme={null}
const res = await fetch(MANIFEST_URL, {
  headers: etag ? { "If-None-Match": etag } : {},
});
if (res.status === 304) return cached;
```

### Response headers

| Header               | On              | Meaning                                                                                          |
| -------------------- | --------------- | ------------------------------------------------------------------------------------------------ |
| `ETag`               | manifest, files | Opaque validator for conditional requests.                                                       |
| `X-TextSetu-Release` | manifest        | The release version — detect a new release without parsing the body.                             |
| `X-TextSetu-Quota`   | manifest, files | Present as `exceeded` when the organization is over its delivery quota. Content is still served. |
| `Retry-After`        | `202`, `429`    | Seconds to wait before retrying.                                                                 |

***

## Status codes

| Status | Meaning                                    | What to do                                                                                                   |
| ------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| `200`  | Manifest or file returned                  | Use it.                                                                                                      |
| `304`  | Unchanged since your `ETag`                | Keep what you have.                                                                                          |
| `202`  | The first release is still building        | Body is `{"status":"building"}`. Honour `Retry-After` (5s) and retry. Sent `no-store` so a CDN can't pin it. |
| `404`  | Unknown key, or the distribution is paused | Body is `{"error":"not_found"}`. Cacheable for 60s.                                                          |
| `429`  | Rate limited, or over quota                | Back off and retry.                                                                                          |

A paused distribution and an unknown key return the **identical** `404` on
purpose — the endpoint is unauthenticated, and distinguishing them would turn it
into an oracle for probing valid keys.

Going over quota does **not** black out your app by default: TextSetu keeps
serving and sets `X-TextSetu-Quota: exceeded`.

***

## Compatibility

What you can rely on:

* **New fields may be added** to the manifest and to `files[]` at any time.
  Ignore what you don't recognise.
* **`minAppVersion`, `maxAppVersion` and `bundles` are reserved.** They're
  always `null`/`[]` today so that version pinning and grouping can be added
  without breaking a client shipped now.
* **`schema` gets a new major version** if anything breaking changes. Check it
  and refuse an unknown major rather than guessing.
* **`languageDetails` is optional.** Always keep a fallback.

***

## Related

* [Content Delivery](/docs/guides/content-delivery) — creating and publishing distributions
* [Using a distribution in your app](/docs/guides/content-delivery-integration) — client code
* [API Reference](/api/introduction) — the generated endpoint reference
