---
title: History
description: "Every save of an App is a revision. List them, read one, and restore an earlier one."
sidebar:
  label: History
  order: 17
---

## What you can do

**See every revision.** Each save of an App makes a new revision. History lists them newest first, with when each was saved, which contract it was written against, and which one is live.

**Read an earlier revision.** Open a revision to see its source: the package's files, or the module, UI, and styles of an App on a dated contract.

**Restore one.** Restoring saves the earlier revision's source as a new revision. Nothing is deleted: the revision you restored over stays in History, so you can restore it back. App data does not change. Connector approvals for services or levels the restored revision does not declare are removed, and have to be given again if you restore back.

**Where it lives.** In App settings, choose **History** (`charm.ing/<owner>/~/apps/<app>/settings/history`). Agents use the MCP tools or the HTTP API below.

**Who can use it.** The App's owner, members of the team that owns it, and anyone the owner invited as an editor. Viewers and visitors cannot.

**How far back.** Your plan decides how many earlier revisions History keeps in reach: 10 on Free, more on paid [plans](/docs/capabilities/limits#plans). Older revisions are kept but cannot be read or restored until the plan reaches them.

## Technical

### Copy this prompt for your agent

```text
Restore my Charming app to an earlier revision. Read
https://charm.ing/docs/capabilities/history.md first. List the
app's revisions, show me what changed in the one I pick, then
restore it and tell me the new revision number.
```

### How an agent performs this job

Over MCP, three tools use the same checks as the routes below: `list_app_revisions({ app_id, before?, limit? })`, `get_app_revision({ app_id, revision })`, and `restore_app_revision({ app_id, revision, expected_revision, idempotency_key? })`. Pass the `currentRevision` from the list as `expected_revision`. A package restore that does not publish at once returns a build; poll it with `get_app_build({ build_id })`. Failures come back as tool errors with the error kinds in the table below.

From the CLI, `charming apps history list <app-id>`, `charming apps history get <app-id> <revision> [--out <dir>]`, and `charming apps history restore <app-id> <revision> --yes [--wait]` run the three routes below. `restore` reads the current revision for `If-Match`, supplies the `Idempotency-Key`, previews the request offline with `--dry-run`, and with `--wait` follows a package revision's build until it publishes.

Over HTTP, three routes under the App, all accepting a user token (`chrm_user_*`) or a session cookie. They are in the [OpenAPI document](https://charm.ing/.well-known/openapi.json) as `list-app-revisions`, `get-app-revision`, and `restore-app-revision`.

| Route | Does |
| --- | --- |
| `GET /api/v1/apps/{appId}/revisions` | Lists revisions newest first. `before` and `limit` (default 50, at most 200) page through them. |
| `GET /api/v1/apps/{appId}/revisions/{revision}` | Returns one revision and its source. |
| `POST /api/v1/apps/{appId}/revisions/{revision}/restore` | Saves that revision's source as a new revision. |

Each listed revision carries `revision`, `createdAt`, `manifestVersion` (the contract, such as `2026-10-25` for a package or `2026-10-16` for a module contract), `current` (the desired revision), `live` (the revision serving visitors), and `restorable`. The list also returns `currentRevision`, `historyLimit` (how many earlier revisions the plan keeps), and `nextBefore`, which you pass as `before` to read the next page; it is `null` on the last page.

`restorable` reflects the checks restore runs on the stored source, and restore runs them again, so a marked revision can still be refused if its source stopped validating in between.

A revision's source is `source.files` for a package revision, or `source.module`, `source.ui`, and `source.styles` for a module-contract revision.

A restore needs the current desired revision as `If-Match: "<N>"` (or `{ "expectedRevision": N }` in the body) and an `Idempotency-Key` header. A module-contract revision publishes immediately and answers 200 with the new `revision`. A package revision publishes from its retained build: usually at once (200), otherwise the response is 202 with a build whose `statusUrl` you poll until `state` is `published`. The App keeps running its current revision until then. Repeating a package restore with the same `Idempotency-Key` returns the same build.

The revision you send is a compare-and-set. If two people, or a person and an agent, restore from the same revision at once, one restore wins and the other answers `revision_mismatch` without changing the App. List History again, check what changed, and restore against the new current revision.

```http
GET /api/v1/apps/3f1c…/revisions?limit=2
Authorization: Bearer chrm_user_…

→ 200 { "ok": true, "currentRevision": 6, "historyLimit": 10, "nextBefore": 5,
        "items": [
          { "revision": 6, "manifestVersion": "2026-10-25", "current": true, "live": true, "restorable": false, … },
          { "revision": 5, "manifestVersion": "2026-10-25", "current": false, "live": false, "restorable": true, … } ] }

POST /api/v1/apps/3f1c…/revisions/5/restore
Authorization: Bearer chrm_user_…
If-Match: "6"
Idempotency-Key: 9b7e2c40-…

→ 200 { "ok": true, "state": "published", "intent": "restore", "sourceRevision": 5, "revision": 7, … }
```

**Across contracts.** An App moved to the package contract can restore a revision from before the move. Charming validates that source under its own dated contract and publishes it as a new revision, so the App goes back to that contract. See [Migrate to packages](/docs/guides/migrate-to-packages).

### Errors

| Error kind | Status | Meaning |
| --- | --- | --- |
| `forbidden` | 403 | The caller can open the App but cannot change it. |
| `not_found` | 404 | The App or revision does not exist, or the caller cannot open the App. |
| `precondition_required` | 428 | The restore sent neither `If-Match` nor `expectedRevision`. |
| `idempotency_key_required` | 400 | The restore sent no `Idempotency-Key`. |
| `revision_mismatch` | 412 | The App was saved or restored since you listed it. The `ETag` header and the error's `current` field carry the current revision. |
| `version_limit_exceeded` | 429 | The revision is older than the plan's History window. The error carries `plan`, `max`, and `upgradeUrl`. |
| `version_not_restorable` | 422 | The revision is missing, has no completed build, or no longer passes its contract. Upgrading does not fix this. |
| `version_history_unavailable` | 409 | The App's contract has no History that can be restored. |
| `app_build_capacity` / `app_build_unavailable` | 429 / 503 | Builds are busy or unavailable. Retry later. |

The plan window is checked first, so a revision that is both out of reach and not restorable answers `version_limit_exceeded`.

Found a bug or need a feature? [Tell us](/docs/capabilities/feedback) with `submit_feedback` or `POST /app/{id}/feedback`. Your feedback shapes what we build next.
