Create an app (anonymous or authenticated upsert)
Anonymous callers get a fresh app row with a chrm_app_* token in the response. Authenticated callers (chrm_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
/appAuthorization
Optional — this operation also accepts unauthenticated requests.
AuthorizationBearer token (chrm_user_*) · headerUser-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/jsonmodulestringrequiredES 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.
uistringOptional inline JS program (classic script, not a module). Populates `#app`.
stylesstringOptional CSS injected alongside `ui`.
descriptionstringOptional app description, shown to the owner and to callers who read it back.
pairbooleanAnonymous 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 `chrm_user_*`.
labelstringAnonymous 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>requiredmanifestIdstringrequireddisplayNamestringrequiredurlstring<uri>requiredcapabilitiescapabilitiesrequiredShow propertiesHide properties
capabilitiesiconIcon | anyShow propertiesHide properties
Any of:
Icon
emojistringrequiredA single emoji, rendered centered (e.g. `⚽`). Extra glyphs are dropped.
bgstringrequiredBackground 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
anyclaimedbooleanrequiredrevisionRevisionrequiredServer-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.
min 0
tokenstringAnonymous-create only. `chrm_app_*` plaintext, returned exactly once — irrecoverable.
expiresAtstring<date-time>Anonymous-create only. 7-day TTL unless claimed.
mcpHintobjectShow propertiesHide properties
setupUrlstring<uri>snippetsobjectpairingobjectPresent only when the request body included `pair: true`. Agent should poll `POST /api/pair/poll` with `device_code` until status flips to `approved`.
Show propertiesHide properties
device_codestringuser_codestringShow verbatim to the user; they type it on `verification_url`.
matches ^CHRM-[A-Z2-9]{6}$
verification_urlstring<uri>polling_intervalintegerexpires_inintegerwarningsstring[]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`.
okbooleanrequiredAllowed:
falseerrorobjectrequiredShow propertiesHide properties
kindstringrequiredStable enum-shaped error key. Branch on this for recovery flows.
Allowed:
not_foundunknown_operationoperation_not_foundoperation_failedunauthorizedsign_in_requiredtoken_expiredforbiddenforbidden_writestored_value_too_largestorage_fullinvalid_moduleinvalid_uiinvalid_route_schemainvalid_routes_shapecontract_migration_requiredinvalid_manifest_schemainvalid_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_limitedimage_origin_deniedtoo_many_requestsexpiredalready_approvedanon_not_remixabletemplate_listing_incompletetemplate_media_invalidforbidden_template_editinvalid_if_matchprecondition_requiredrevision_mismatchinvalid_revisioncandidate_effects_unsupportedold_string_not_foundold_string_not_uniqueedits_too_largemanifest_id_conflictmanifest_id_immutabletext_too_largestructured_data_too_largeapp_not_foundfeedback_not_foundfeedback_has_no_reply_recipientasset_too_largeasset_count_exceededasset_quota_exceededasset_invalid_keyasset_invalid_content_typeasset_errorinvalid_idsse_cookie_expiredinvalid_secret_namesecret_too_largesecret_already_existssecret_not_foundinvalid_iconteam_slug_takenalready_membergrantee_not_foundinvite_cap_reachedread_onlyhandle_takeninvalid_handlename_takenapp_unclaimedlink_cap_reachedgrantee_is_ownergrantee_is_selfgrantee_already_has_accessalready_invitedroutine_limit_exceededinvalid_intervalop_requires_inputduplicate_routinemessagestringreasonstringSet on 401s to distinguish token failure modes when finer detail is needed. `token_revoked_post_claim` is the post-#1284 surface for a chrm_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_claimrecoveryRecoveryPair | RecoveryRetry | RecoveryShrink | RecoveryRefetch | RecoveryFixRouteLabel | RecoveryOpenExisting | RecoveryRename | RecoveryMigrateContractSelf-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 `revision_mismatch` / `invalid_if_match` / 428 `precondition_required`, `fix_route_label` for 403 `forbidden_write`, `open_existing` for 409 `duplicate_remix`, `rename` for 409 `manifest_id_immutable` / `manifest_id_conflict`, and `migrate_contract` for 422 `contract_migration_required`. 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 propertiesHide properties
One of:
RecoveryPair
kindstringrequiredAllowed:
pairstartstring<uri>requiredverification_urlstring<uri>requiredinstructionsstringrequiredRecoveryRetry
kindstringrequiredAllowed:
retryretry_after_secintegerrequiredSeconds to wait before retrying. Matches the `RateLimit-Reset` and `Retry-After` headers on the same response.
min 0
retry_after_urlstring<uri>requiredAbsolute URL to retry verbatim once the wait elapses.
instructionsstringrequiredRecoveryShrink
kindstringrequiredAllowed:
shrinkfieldstringrequiredWhich input tripped the cap.
Allowed:
moduleuistylesdescriptiontextstructured_dataasseteditssecretpayloadsize_bytesintegerrequiredmin 0
cap_bytesintegerrequiredmin 0
instructionsstringrequiredRecoveryRefetch
kindstringrequiredAllowed:
refetchrefetch_urlstring<uri>requiredAbsolute URL to GET for the fresh ETag before retrying with a corrected If-Match.
instructionsstringrequiredRecoveryFixRouteLabel
kindstringrequiredAllowed:
fix_route_labelinstructionsstringrequiredApp-code recovery: the fix is in the route declaration, NOT in the request. Retrying the same request will fail again.
RecoveryOpenExisting
kindstringrequiredAllowed:
open_existingexisting_app_idstringrequiredexisting_urlstring<uri>requiredinstructionsstringrequiredRecoveryRename
kindstringrequiredAllowed:
renameinstructionsstringrequiredRecoveryMigrateContract
kindstringrequiredAllowed:
migrate_contractinstructionsstringrequiredTry it
Server
Authorization (optional)
Bodyapplication/json
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",
"description": "string",
"pair": true,
"label": "string"
}'const response = await fetch("https://charm.ing/app", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({
"module": "string",
"ui": "string",
"styles": "string",
"description": "string",
"pair": true,
"label": "string"
})
});import requests
response = requests.post(
"https://charm.ing/app",
headers={
"Authorization": "Bearer YOUR_TOKEN",
"Content-Type": "application/json"
},
json={
"module": "string",
"ui": "string",
"styles": "string",
"description": "string",
"pair": True,
"label": "string"
},
)Response
{
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"manifestId": "string",
"displayName": "string",
"url": "http://example.com",
"capabilities": {
"imports": [
"charming:storage/[email protected]"
]
},
"icon": {
"emoji": "string",
"bg": "string"
},
"claimed": true,
"revision": 4,
"token": "string",
"expiresAt": "2019-08-24T14:15:22Z",
"mcpHint": {
"setupUrl": "http://example.com",
"snippets": {
"property1": "string",
"property2": "string"
}
},
"pairing": {
"device_code": "string",
"user_code": "string",
"verification_url": "http://example.com",
"polling_interval": 0,
"expires_in": 0
},
"warnings": [
"string"
]
}{
"ok": false,
"error": {
"kind": "not_found",
"message": "string",
"reason": "token_malformed",
"recovery": {
"kind": "pair",
"start": "http://example.com",
"verification_url": "http://example.com",
"instructions": "string"
}
}
}