Skip to content
Charming Docs
Esc
navigateopen⌘Jpreview

Create an app (anonymous or authenticated upsert)

Anonymous callers get a fresh app row with a bld_app_* token in the response. Authenticated callers (bld_user_* or session) upsert by (userId, manifestId) — second POSTs with the same manifest.id overwrite the existing row in place and the response includes the same id. Use PUT /app/{id} for deliberate updates against a known id. Pass pair: true to mint a bound device_code in the same call.

POST/app
Authorization

Optional — this operation also accepts unauthenticated requests.

AuthorizationBearer token (bld_user_*) · header
User-scoped personal access token. Mint via `POST /api/token` after signing in, or via the device-pairing flow at `POST /api/pair/start`. Authorises everything the user can do.
Request body
requiredapplication/json
modulestringrequired
ES module source. Must export a literal `manifest`. May export `routes` and/or `default.fetch`; when `default.fetch` is absent, Charming supplies a generic 404 handler.
uistring
Optional inline JS program (classic script, not a module). Populates `#app`.
stylesstring
Optional CSS injected alongside `ui`.
pairboolean
Anonymous POST only — silently ignored on authenticated upserts. When true, server mints a bound device_code alongside the app token; user-claim auto-approves the pairing and the agent ends up with both an app token and a `bld_user_*`.
labelstring
Anonymous POST only — silently ignored on authenticated upserts. Human-readable label for the agent, surfaced to the user on the `/pair` approval page when `pair: true`.
max length 80
Responses
201App created or upserted.
idstring<uuid>required
manifestIdstringrequired
displayNamestringrequired
urlstring<uri>required
capabilitiescapabilitiesrequired
Show properties
capabilities
iconIcon | any
Show properties
Any of:
Icon
emojistringrequired
A single emoji, rendered centered (e.g. `⚽`). Extra glyphs are dropped.
bgstringrequired
Background color as a hex string (`#rgb`, `#rrggbb`, or `#rrggbbaa`). Named colors (`"green"`), `rgb()`/`hsl()`, and image URLs are rejected.
matches ^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$
any
any
claimedbooleanrequired
tokenstring
Anonymous-create only. `bld_app_*` plaintext, returned exactly once — irrecoverable.
expiresAtstring<date-time>
Anonymous-create only. 7-day TTL unless claimed.
mcpHintobject
Show properties
setupUrlstring<uri>
snippetsobject
pairingobject
Present only when the request body included `pair: true`. Agent should poll `POST /api/pair/poll` with `device_code` until status flips to `approved`.
Show properties
device_codestring
user_codestring
Show verbatim to the user; they type it on `verification_url`.
matches ^BLDY-[A-Z2-9]{6}$
verification_urlstring<uri>
polling_intervalinteger
expires_ininteger
warningsstring[]
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.
400Invalid module / ui / unexpected body keys / capability_denied. See `error.kind`.
okbooleanrequired
Allowed:false
errorobjectrequired
Show properties
kindstringrequired
Stable enum-shaped error key. Branch on this for recovery flows.
Allowed:not_foundunknown_operationoperation_not_foundoperation_failedunauthorizedtoken_expiredforbiddenforbidden_writestorage_quota_exceededinvalid_moduleinvalid_uiinvalid_route_schemainvalid_routes_shapeinvalid_manifest_exportsinvalid_requestinvalid_inputinvalid_outputunexpected_keyscapability_deniedcapability_errorexecutor_errorinvalid_call_shapealready_claimedduplicate_remixmanifest_not_foundpayload_too_largemodule_too_largeui_too_largedescription_too_longmissing_descriptioninvalid_descriptionrate_limitedtoo_many_requestsexpiredalready_approvedanon_not_remixableinvalid_if_matchprecondition_requiredversion_mismatchold_string_not_foundold_string_not_uniqueedits_too_largemanifest_id_conflictmanifest_id_immutabletext_too_largestructured_data_too_largeapp_not_foundasset_too_largeasset_count_exceededasset_quota_exceededasset_invalid_keyasset_invalid_content_typeasset_errorinvalid_idsse_cookie_expiredinvalid_secret_namesecret_too_largesecret_already_existsinvalid_iconteam_slug_takenalready_membergrantee_not_foundinvite_cap_reachedread_onlyhandle_takeninvalid_handlename_takenapp_unclaimedlink_cap_reachedgrantee_is_owneralready_invited
messagestring
reasonstring
Set on 401s to distinguish token failure modes when finer detail is needed. `token_revoked_post_claim` is the post-#1284 surface for a bld_app_* that was invalidated by a claim — the `recovery` field on the same `error` object carries the self-contained pairing-recovery hint the agent should follow instead of creating a new app.
Allowed:token_malformedtoken_unknowntoken_revokedtoken_revoked_post_claim
recoveryRecoveryPair | RecoveryRetry | RecoveryShrink | RecoveryRefetch | RecoveryFixRouteLabel | RecoveryOpenExisting | RecoveryRename
Self-contained recovery hint, branching on `kind`. Attached to error envelopes the agent can self-correct on: `pair` for `token_revoked_post_claim` 401s (#1284), `retry` for 429s, `shrink` for too-large / quota-exceeded 413s, `refetch` for 412 `version_mismatch` / `invalid_if_match` / 428 `precondition_required`, `fix_route_label` for 403 `forbidden_write`, `open_existing` for 409 `duplicate_remix`, and `rename` for 409 `manifest_id_immutable` / `manifest_id_conflict`. Every `instructions` string is a single-paragraph, action-first walk-through. Inlined per ADR 2026-05-06-agent-facing-error-surfaces-self-contain-the-fix.
Show properties
One of:
RecoveryPair
kindstringrequired
Allowed:pair
startstring<uri>required
verification_urlstring<uri>required
instructionsstringrequired
RecoveryRetry
kindstringrequired
Allowed:retry
retry_after_secintegerrequired
Seconds to wait before retrying. Matches the `RateLimit-Reset` and `Retry-After` headers on the same response.
min 0
retry_after_urlstring<uri>required
Absolute URL to retry verbatim once the wait elapses.
instructionsstringrequired
RecoveryShrink
kindstringrequired
Allowed:shrink
fieldstringrequired
Which input tripped the cap.
Allowed:moduleuistylesdescriptiontextstructured_dataasseteditssecretpayload
size_bytesintegerrequired
min 0
cap_bytesintegerrequired
min 0
instructionsstringrequired
RecoveryRefetch
kindstringrequired
Allowed:refetch
refetch_urlstring<uri>required
Absolute URL to GET for the fresh ETag before retrying with a corrected If-Match.
instructionsstringrequired
RecoveryFixRouteLabel
kindstringrequired
Allowed:fix_route_label
instructionsstringrequired
App-code recovery: the fix is in the route declaration, NOT in the request. Retrying the same request will fail again.
RecoveryOpenExisting
kindstringrequired
Allowed:open_existing
existing_app_idstringrequired
existing_urlstring<uri>required
instructionsstringrequired
RecoveryRename
kindstringrequired
Allowed:rename
instructionsstringrequired
Request
curl -X POST "https://charm.ing/app" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "module": "string",
  "ui": "string",
  "styles": "string",
  "pair": true,
  "label": "string"
}'
Response
{
  "id": "<uuid>",
  "manifestId": "string",
  "displayName": "string",
  "url": "<uri>",
  "capabilities": null,
  "icon": {
    "emoji": "string",
    "bg": "string"
  },
  "claimed": true,
  "token": "string",
  "expiresAt": "2024-01-01T00:00:00Z",
  "mcpHint": {
    "setupUrl": "<uri>",
    "snippets": {}
  },
  "pairing": {
    "device_code": "string",
    "user_code": "string",
    "verification_url": "<uri>",
    "polling_interval": 0,
    "expires_in": 0
  },
  "warnings": [
    "string"
  ]
}