---
title: "Create an app or accept an ESM app build"
method: POST
path: "/app"
---

# Create an app or accept an ESM app build

`POST /app`

## Request

- Base URL: `https://charm.ing`
- URL: `https://charm.ing/app`
- Auth: one of:
  - none
  - HTTP bearer

Existing-contract callers receive a published app synchronously; anonymous creation returns a chrm_app_* token and may request pair: true. The explicit https://charm.ing/schema/app-manifest/next.json contract requires an enabled authenticated user and Idempotency-Key, validates imports without evaluation, and returns 202 before dependency resolution or compilation. A same-owner manifest.id target is frozen at acceptance; an absent target cannot later become an overwrite. Use PUT for a known app. Every existing ESM destination requires its desired revision in If-Match; migration also requires migrate_contract: true. pair and label are not ESM fields.

## Headers

- `Idempotency-Key` string
- `If-Match` string

## Request body

- AppCreateBody
  - `module` string, required: Server ES module source. Must export a literal canonical `manifest` and a `routes` array. A route handler returns exactly the value declared by `outputSchema`; for `outputSchema: { type: "array", items: ... }`, return the array directly. Charming adds the HTTP transport envelope, so do not add `{ ok, value }` or `{ value }` unless those fields belong to `outputSchema` itself. `default.fetch` is an optional unmatched-request fallback; when absent, Charming supplies a generic 404 handler. Existing contracts publish synchronously; the explicit https://charm.ing/schema/app-manifest/next.json contract requires an enabled authenticated user and accepts a background build.
  - `ui` string, nullable: Optional browser source populating #app. A classic script for the existing contract; an ES module for the explicit ESM contract. ESM bare imports must match manifest.dependencies.client.
  - `styles` string, nullable: Optional CSS injected alongside ui.
  - `description` string, nullable: Optional app description, shown to the owner and to callers who read it back. For ESM POST upserts, omission preserves the desired description.
  - `migrate_contract` boolean: ESM POST only: true explicitly migrates an existing same-owner manifest.id target. Supply its desired revision in If-Match. The existing app remains active until the build publishes.
  - `pair` boolean: Existing-contract anonymous POST only; rejected on ESM saves. Silently ignored on existing-contract authenticated upserts. When true, server mints a bound device_code alongside the app token; claiming the app approves the pairing, and approving the pairing code at `/pair` claims the app, so either gesture gives the agent a `chrm_user_*` for the claimed app.
  - `label` string: Existing-contract anonymous POST only; rejected on ESM saves. Silently ignored on existing-contract authenticated upserts. Human-readable label for the agent, surfaced to the user on the `/pair` approval page when `pair: true`.

## Response `201`

App created or upserted.

- AppCreateResponse
  - `id` string, uuid, required
  - `manifestId` string, required
  - `displayName` string, required
  - `url` string, uri, required
  - `capabilities` Capabilities, required (unresolved $ref)
  - `icon` Icon: Optional home-screen / favicon icon. The server composes a colored rounded-square PNG/SVG from `emoji + bg` — NOT a list of image URLs like a W3C web manifest. Omit it to get the default icon. If `emoji` or `bg` is invalid the whole icon is silently dropped (stored as unset, so the default renders) and the create/update response carries a `warnings[]` entry. This warn-and-drop contract is the `manifest.icon` publish path only (`PUT /app/{id}` and its create counterpart) — `PUT /app/{id}/icon` uses the stricter `IconStrict` schema instead.
    - `emoji` string, required: A single emoji, rendered centered (e.g. `⚽`). Extra glyphs are dropped.
    - `bg` string, required: Background color as a hex string (`#rgb`, `#rrggbb`, or `#rrggbbaa`). Named colors (`"green"`), `rgb()`/`hsl()`, and image URLs are rejected.
  - `claimed` boolean, required
  - `revision` integer, required: Server-owned app source revision. New apps start at 1, each successful source write advances it once, and historical null counters project as 0. `If-Match: "<N>"` and `expected_revision` carry this revision through its canonical concurrency grammar.
  - `token` string: Anonymous-create only. `chrm_app_*` plaintext, returned exactly once — irrecoverable.
  - `expiresAt` string, date-time: Anonymous-create only. 7-day TTL unless claimed.
  - `mcpHint` object
    - `setupUrl` string, uri
    - `snippets` object
  - `pairing` object: Present only when the request body included `pair: true`. Agent should poll `POST /api/pair/poll` with `device_code` until status flips to `approved`.
    - `device_code` string
    - `user_code` string: Show verbatim to the user; they type it on `verification_url`.
    - `verification_url` string, uri
    - `polling_interval` integer
    - `expires_in` integer
  - `warnings` string[]: Non-blocking publish feedback (#1126), present only when static validation found UI/backend contract mismatches — e.g. the UI calls an operation this version does not expose. The write succeeded; fix by adding the backend op or renaming the UI call.

## Other responses

- `202`: ESM source accepted for a background build. This response does not mean the app has published. Poll statusUrl until a terminal state; new creation has no runnable app or URL until published.
- `400`: Invalid source syntax, body, or required ESM idempotency key. See error.kind.
- `401`: ESM creation requires a valid user credential.
- `403`: ESM authoring is not enabled for this caller (contract_not_enabled).
- `409`: Idempotency conflict or frozen manifest identity conflict.
- `412`: The desired source revision changed.
- `413`: Source exceeds the contract limits.
- `422`: Manifest, import/dependency mismatch, or explicit migration validation failed.
- `428`: A required desired-source revision is missing.
- `429`: Build capacity is full (app_build_capacity). Retry-After gives the retry delay.

---

[API](https://skmtc.dev/charm/apis/charming-api.md) · [All operations](https://skmtc.dev/charm/apis/charming-api/llms.txt) · [OpenAPI document](https://skmtc.dev/charm/apis/charming-api/revisions/a9d06c419b3a?raw)
