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

# Editing Theme Tokens in the Livry Public API

> Read a Theme's DTCG token document, edit it with RFC 6902 operations, publish an immutable version, or discard the draft.

A Theme's token set is a **W3C DTCG document** — nested groups, `$value`, `$type`, `$description`,
`$extensions`, and `{alias}` references left unresolved. See [Tokens](/theming/tokens).

| | | Needs |
| - | - | - |
| `GET` | `…/themes/{themeID}/tokens` | `viewer` |
| `PATCH` | `…/themes/{themeID}/tokens` | `editor` |
| `POST` | `…/themes/{themeID}/tokens/publish` | `editor` |
| `DELETE` | `…/themes/{themeID}/tokens` | `editor` |

All paths are under `/public/v1/environments/{environmentID}`.

## The draft-and-versions model

A Theme holds **zero or one draft** and **N immutable versions**.

<Steps>
  <Step title="An edit creates the draft">
    The first `PATCH` that changes something creates a draft. An edit that reverted itself leaves
    nothing behind, so `hasUnpublishedChanges` is exactly "is there a draft".
  </Step>

  <Step title="Publishing freezes it">
    `POST …/publish` writes the draft as the next version and **deletes the draft**. Version numbers
    start at 1 and never skip.
  </Step>

  <Step title="Versions are never editable">
    Nothing in Livry can write an existing version. A pinned URL's content cannot change.
  </Step>
</Steps>

`DELETE …/tokens` throws the draft away and leaves every version alone. It is a discard, not a
deletion of the Theme's tokens.

## Read the tokens

```http theme={null}
GET …/themes/{themeID}/tokens
```

<ParamField query="version" type="integer">
  Read a specific published version instead of the current document. Omit for "what is current" —
  the draft if there is one, otherwise the newest version.
</ParamField>

<ParamField query="includeVersions" type="boolean" default="false">
  Also list every published version number, newest first. Listing costs a storage request of its own,
  which is why it is opt-in.
</ParamField>

```json Response 200 theme={null}
{
  "resource": {
    "version": null,
    "latestVersion": 7,
    "publishedAt": "2026-09-17T22:10:44Z",
    "etag": "0x8DC1F2A9B3E4C51",
    "hasUnpublishedChanges": true,
    "versions": [],
    "tokens": [
      {
        "path": "color.brand.primary",
        "type": null,
        "resolvedType": "color",
        "inferred": false,
        "value": {
          "colorSpace": "srgb",
          "components": [0.357, 0.302, 0.894],
          "hex": "#5B4DE4"
        },
        "description": "The primary action colour.",
        "extensions": null
      },
      {
        "path": "color.brand.hover",
        "type": null,
        "resolvedType": null,
        "inferred": false,
        "value": "{color.brand.primary}",
        "description": null,
        "extensions": null
      }
    ],
    "groups": [
      {
        "path": "color.brand",
        "type": "color",
        "description": "Everything derived from the customer's brand mark."
      }
    ]
  }
}
```

### Reading that response

<ResponseField name="etag" type="string | null">
  **The concurrency token for your next write**, and it is the draft's — not the Theme entity's.
  `null` means there is no draft, which is what a first edit legitimately sends.
</ResponseField>

<ResponseField name="tokens" type="array">
  Every token, flattened onto a dotted path, depth-first in document order. Not sorted — the order is
  the document's.
</ResponseField>

<ResponseField name="groups" type="array">
  **Only the groups that carry metadata of their own.** A group implied purely by the paths beneath
  it tells you nothing the paths do not, and listing every one would roughly double the payload. A
  group with a `$type` is the case this exists for: that type is a statement about every token under
  it, and it is not recoverable from the paths.
</ResponseField>

<Warning>
  **`type` and `resolvedType` are two different questions, and you may only write the first.**

  `type` is the token's own `$type`, `null` when it inherits one from a group. `resolvedType` is what
  the token actually is after inheritance and inference.

  Writing `resolvedType` back — materialising an inherited type onto the token — stops its group ever
  retyping its children. The API will let you; the document will be wrong. Address `/…/$type`.
</Warning>

`inferred` tells you whether `resolvedType` was guessed from the value's shape rather than declared
anywhere.

## Edit the tokens

```http theme={null}
PATCH …/themes/{themeID}/tokens?etag={etag}
```

**Tokens are edited through operations, never by sending the document.** A whole-document write makes
a deletion mean *omission*, so a client holding a stale copy would silently delete everything added
since it read.

The batch is applied to what is current, and the stored patch is re-derived from the result — so your
call is additive while the stored file stays the minimal distance from the version below it.

<CodeGroup>
  ```json Request theme={null}
  {
    "operations": [
      {
        "operation": "add",
        "path": "/color/brand/accent",
        "value": { "$value": "#18AE59" }
      },
      {
        "operation": "replace",
        "path": "/color/brand/primary/$value",
        "value": "#5B4DE4"
      },
      {
        "operation": "replace",
        "path": "/color/brand/$type",
        "value": "color"
      },
      {
        "operation": "move",
        "from": "/color/brand/hover",
        "path": "/color/brand/primaryHover"
      },
      {
        "operation": "remove",
        "path": "/color/legacy"
      }
    ]
  }
  ```

  ```bash curl theme={null}
  curl -X PATCH "$BASE/environments/$ENV/themes/$THEME/tokens?etag=$ETAG" \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d @batch.json
  ```
</CodeGroup>

Returns the whole token set as it stands afterwards, including the **new `etag`** your next write
needs.

### The operations

These are RFC 6902's five, addressed by RFC 6901 JSON Pointers. They are not our invention and we
have not renamed them.

| `operation` | Carries | Refuses |
| - | - | - |
| `add` | `path`, `value` | `from` |
| `replace` | `path`, `value` | `from` |
| `remove` | `path` | `value`, `from` |
| `move` | `path`, `from` | `value` |
| `copy` | `path`, `from` | `value` |

A pointer addresses the document, so `/color/brand/primary` is the token and
`/color/brand/primary/$value` is its value. Escape `/` as `~1` and `~` as `~0`, as RFC 6901 requires.

<Note>
  **Prefer `move` over remove-and-add for a rename.** When a Theme publishes, every Variant beneath it
  is migrated onto the new structure — keys the Theme renamed are renamed, keys it removed are
  dropped. A `move` says plainly that a rename happened; a remove-and-add pair has to be inferred,
  and the inference can lose an override when the rename also changed the value.
</Note>

## Publish a version

```http theme={null}
POST …/themes/{themeID}/tokens/publish
```

No body. Returns the Theme — with `latestVersion` set to the version just written and
`hasUnpublishedChanges` back to `false`.

Publishing also:

* writes the served files for every (Theme, Variant) pair at the new version, and moves `latest`;
* **migrates every Variant's draft** onto the new structure, renaming the keys the Theme renamed and
  dropping the ones it removed or retyped past fitting.

<Warning>
  Migration corrects each Variant's **draft**. It does not publish them. After a Theme publish, your
  Variants have unpublished changes they did not ask for — review and publish each one.
</Warning>

## Discard the draft

```http theme={null}
DELETE …/themes/{themeID}/tokens?etag={etag}
```

Throws the draft away and returns the Theme. Published versions are untouched. Send the `etag` so
you cannot discard a draft that changed under you.

## Two DTCG features Livry does not apply

<Warning>
  `$extends` (group inheritance) and `$ref` (JSON Pointer references, including into a value) are
  **preserved verbatim so documents round-trip, but their semantics are never applied.** A document
  that relies on either will import with tokens missing, and nothing will warn you.

  Aliases — `{color.brand.primary}` — *are* supported, and are resolved in the served `tokens.json`,
  `tokens.css` and `tokens.flat.json`. `tokens.dtcg.json` keeps them as written.
</Warning>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.