Skip to content
Esc
↑↓navigate↵open⌘Jpreview

Create an app or accept an ESM app build

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.

POST/app
Authorization

Optional — this operation also accepts unauthenticated requests.

AuthorizationBearer token (chrm_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. Accepted by every operation that lists userToken in its security; operations that list only sessionCookie refuse it with 401 unauthorized.

Header parameters
Idempotency-Keystring

Required for ESM saves. Reuse the same key, body, target and If-Match after response loss; a changed request with that key returns idempotency_conflict. Use a fresh key for each intentional save.

min length 8 · max length 128 · matches ^[!-~]+$
If-Matchstring

Desired-source revision in double quotes for an ESM upsert or migration. Read GET /app/{id}/source first.

Request body
requiredapplication/json
modulestringrequired

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.

uistring | null

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.

stylesstring | null

Optional CSS injected alongside ui.

descriptionstring | null

Optional app description, shown to the owner and to callers who read it back. For ESM POST upserts, omission preserves the desired description.

migrate_contractboolean

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.

pairboolean

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.

labelstring

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.

max length 80
Responses
201

App created or upserted.

idstring<uuid>required
manifestIdstringrequired
displayNamestringrequired
urlstring<uri>required
capabilitiescapabilitiesrequired
Show properties
capabilities
iconIcon | null
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})$
null
null
claimedbooleanrequired
revisionRevisionrequired

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.

min 0
tokenstring

Anonymous-create only. chrm_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 ^CHRM-[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.

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.

okbooleanrequired
Allowed:true
buildIdstringrequired
intentstringrequired
Allowed:createupdatemigraterestorecopy
statestringrequired
Allowed:queuedresolvingbuildingvalidatingpublishedfailedsupersededcanceledexpired
sourceEtagstringrequired
statusUrlstring<uri>required
retryAfterSecondsinteger
appIdstring<uuid>
revisioninteger

Published app source revision. Present only after successful publication.

min 1
desiredRevisioninteger
activeRevisioninteger
urlstring<uri>
attemptsintegerrequired
acceptedAtstring<date-time>required
updatedAtstring<date-time>required
finishedAtstring<date-time> | nullrequired
elapsedMsintegerrequired
lockStatestringrequired
Allowed:lockedunresolved
inputDigeststringrequired
lockDigeststring
resolvedDependenciesobject
Show properties
serverobjectrequired
clientobjectrequired
queueDeadlinestring<date-time>required
deadlinestring<date-time>required
inspectionExpiresAtstring<date-time> | nullrequired
idempotencyExpiresAtstring<date-time> | nullrequired
sourceobject
Show properties
modulestringrequired
uistring | nullrequired
stylesstring | nullrequired
descriptionstring | nullrequired
errorobject
Show properties
kindstringrequired
messagestringrequired
retryablebooleanrequired
detailsobject
Show properties
Any of:
object
executionstringrequired
Allowed:not_startedmay_have_run
operationIdstringrequired
min length 1
reservationIdstring
min length 1
object
executionstringrequired
Allowed:may_have_run
targetstring
specifierstring
lineinteger
columninteger
400

Invalid source syntax, body, or required ESM idempotency key. 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_foundmethod_not_allowedoperation_failedapp_busyvisitor_rate_limitedservice_busyunauthorizedsign_in_requiredtoken_expiredforbiddenaccount_not_joinedforbidden_writestored_value_too_largestorage_fullinvalid_moduleinvalid_uiunpinned_cdn_scriptinvalid_route_schemainvalid_routes_shapecontract_migration_requiredinvalid_manifest_schemainvalid_manifest_exportsinvalid_requestinvalid_inputinvalid_outputunexpected_keyscapability_deniedcapability_errorexecutor_errorinvalid_call_shapealready_claimedmanifest_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_requiredexpected_revision_requiredidempotency_key_requiredidempotency_conflictcontract_not_enabledapp_build_capacityapp_build_failedapp_build_expiredapp_build_authority_lostapp_build_unavailableapp_build_unsupportedversion_history_unavailableversion_not_restorablerevision_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_reachedinvite_sender_blockedread_onlyhandle_takeninvalid_handlename_takeninvalid_domaindomain_takendomain_existsdomain_provider_unavailableapp_unclaimedlink_cap_reachedgrantee_is_ownergrantee_is_selfgrantee_already_has_accessalready_invitedroutine_limit_exceededinvalid_intervalop_requires_inputduplicate_routinewebhook_limit_exceededduplicate_webhookwebhook_disabledversion_limit_exceededplan_requiredstorage_limit_exceededcopy_data_forbiddencopy_data_too_large
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 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_claim
planstring

Present on every plan refusal: the tier the caller was refused under. Its presence is what marks an error as a plan refusal rather than a validation or permission failure.

Allowed:freeprobusinessenterprise
limitstring | integer

Which ceiling the call met, keyed by kind. A plan refusal (*_limit_exceeded) names the countable that ran out — version_history, routines_per_app, routines_per_owner, webhooks_per_app, webhooks_per_owner — and pairs it with max. An app_busy refusal names the bound it met and pairs it with observed and allowed; no plan lifts any of them. Two are per-app run bounds, concurrent_runs and runs_per_window; the third, render_token_refresh, is the page render asking for a new render-token secret, held by 10 per app per client IP and 60 per app across every address in a 60 second window, and observed/allowed name whichever of those two refused. An edits_too_large rejection carries the numeric cap that was exceeded instead of a name.

rungstring

On an app_busy refusal, how hard the app's own refusal ladder is refusing: throttled is the plain ceiling breach above; parked means a caller kept calling inside a wait it was given, so the server mints its own Retry-After (doubling from 60s to a 300s cap) and refuses until a quiet window passes even once the ceiling clears; closed is the same, plus the response closes the connection. Absent on every other kind.

Allowed:throttledparkedclosed
maxinteger

On a *_limit_exceeded refusal, that limit's number on the caller's plan.

min 0
featurestring

On a plan_required refusal, the capability the plan does not include. Never paired with limit / max.

Allowed:custom_domainwatermark_removalteams
upgradeUrlstring<uri>

Absolute link to the owner's own billing page, where the refusal is lifted — the team's page for a team-owned app. Omitted only when the owner has no handle and therefore no settings page of their own, so treat it as optional and fall back to your own copy. MCP appends it as an Upgrade: <url> last line; the CLI prints it under the message.

recoveryRecoveryPair | RecoveryRetry | RecoveryShrink | RecoveryRefetch | RecoveryFixRouteLabel | RecoveryRename | RecoveryMigrateContract

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 revision_mismatch / invalid_if_match / 428 precondition_required, fix_route_label for 403 forbidden_write, 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 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.

RecoveryRename
kindstringrequired
Allowed:rename
instructionsstringrequired
RecoveryMigrateContract
kindstringrequired
Allowed:migrate_contract
instructionsstringrequired
401

ESM creation requires a valid user credential.

okbooleanrequired
Allowed:false
errorobjectrequired
Show properties
kindstringrequired

Stable enum-shaped error key. Branch on this for recovery flows.

Allowed:not_foundunknown_operationoperation_not_foundmethod_not_allowedoperation_failedapp_busyvisitor_rate_limitedservice_busyunauthorizedsign_in_requiredtoken_expiredforbiddenaccount_not_joinedforbidden_writestored_value_too_largestorage_fullinvalid_moduleinvalid_uiunpinned_cdn_scriptinvalid_route_schemainvalid_routes_shapecontract_migration_requiredinvalid_manifest_schemainvalid_manifest_exportsinvalid_requestinvalid_inputinvalid_outputunexpected_keyscapability_deniedcapability_errorexecutor_errorinvalid_call_shapealready_claimedmanifest_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_requiredexpected_revision_requiredidempotency_key_requiredidempotency_conflictcontract_not_enabledapp_build_capacityapp_build_failedapp_build_expiredapp_build_authority_lostapp_build_unavailableapp_build_unsupportedversion_history_unavailableversion_not_restorablerevision_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_reachedinvite_sender_blockedread_onlyhandle_takeninvalid_handlename_takeninvalid_domaindomain_takendomain_existsdomain_provider_unavailableapp_unclaimedlink_cap_reachedgrantee_is_ownergrantee_is_selfgrantee_already_has_accessalready_invitedroutine_limit_exceededinvalid_intervalop_requires_inputduplicate_routinewebhook_limit_exceededduplicate_webhookwebhook_disabledversion_limit_exceededplan_requiredstorage_limit_exceededcopy_data_forbiddencopy_data_too_large
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 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_claim
planstring

Present on every plan refusal: the tier the caller was refused under. Its presence is what marks an error as a plan refusal rather than a validation or permission failure.

Allowed:freeprobusinessenterprise
limitstring | integer

Which ceiling the call met, keyed by kind. A plan refusal (*_limit_exceeded) names the countable that ran out — version_history, routines_per_app, routines_per_owner, webhooks_per_app, webhooks_per_owner — and pairs it with max. An app_busy refusal names the bound it met and pairs it with observed and allowed; no plan lifts any of them. Two are per-app run bounds, concurrent_runs and runs_per_window; the third, render_token_refresh, is the page render asking for a new render-token secret, held by 10 per app per client IP and 60 per app across every address in a 60 second window, and observed/allowed name whichever of those two refused. An edits_too_large rejection carries the numeric cap that was exceeded instead of a name.

rungstring

On an app_busy refusal, how hard the app's own refusal ladder is refusing: throttled is the plain ceiling breach above; parked means a caller kept calling inside a wait it was given, so the server mints its own Retry-After (doubling from 60s to a 300s cap) and refuses until a quiet window passes even once the ceiling clears; closed is the same, plus the response closes the connection. Absent on every other kind.

Allowed:throttledparkedclosed
maxinteger

On a *_limit_exceeded refusal, that limit's number on the caller's plan.

min 0
featurestring

On a plan_required refusal, the capability the plan does not include. Never paired with limit / max.

Allowed:custom_domainwatermark_removalteams
upgradeUrlstring<uri>

Absolute link to the owner's own billing page, where the refusal is lifted — the team's page for a team-owned app. Omitted only when the owner has no handle and therefore no settings page of their own, so treat it as optional and fall back to your own copy. MCP appends it as an Upgrade: <url> last line; the CLI prints it under the message.

recoveryRecoveryPair | RecoveryRetry | RecoveryShrink | RecoveryRefetch | RecoveryFixRouteLabel | RecoveryRename | RecoveryMigrateContract

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 revision_mismatch / invalid_if_match / 428 precondition_required, fix_route_label for 403 forbidden_write, 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 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.

RecoveryRename
kindstringrequired
Allowed:rename
instructionsstringrequired
RecoveryMigrateContract
kindstringrequired
Allowed:migrate_contract
instructionsstringrequired
403

ESM authoring is not enabled for this caller (contract_not_enabled), or the caller is an App account that has not joined Charming (account_not_joined).

okbooleanrequired
Allowed:false
errorobjectrequired
Show properties
kindstringrequired

Stable enum-shaped error key. Branch on this for recovery flows.

Allowed:not_foundunknown_operationoperation_not_foundmethod_not_allowedoperation_failedapp_busyvisitor_rate_limitedservice_busyunauthorizedsign_in_requiredtoken_expiredforbiddenaccount_not_joinedforbidden_writestored_value_too_largestorage_fullinvalid_moduleinvalid_uiunpinned_cdn_scriptinvalid_route_schemainvalid_routes_shapecontract_migration_requiredinvalid_manifest_schemainvalid_manifest_exportsinvalid_requestinvalid_inputinvalid_outputunexpected_keyscapability_deniedcapability_errorexecutor_errorinvalid_call_shapealready_claimedmanifest_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_requiredexpected_revision_requiredidempotency_key_requiredidempotency_conflictcontract_not_enabledapp_build_capacityapp_build_failedapp_build_expiredapp_build_authority_lostapp_build_unavailableapp_build_unsupportedversion_history_unavailableversion_not_restorablerevision_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_reachedinvite_sender_blockedread_onlyhandle_takeninvalid_handlename_takeninvalid_domaindomain_takendomain_existsdomain_provider_unavailableapp_unclaimedlink_cap_reachedgrantee_is_ownergrantee_is_selfgrantee_already_has_accessalready_invitedroutine_limit_exceededinvalid_intervalop_requires_inputduplicate_routinewebhook_limit_exceededduplicate_webhookwebhook_disabledversion_limit_exceededplan_requiredstorage_limit_exceededcopy_data_forbiddencopy_data_too_large
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 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_claim
planstring

Present on every plan refusal: the tier the caller was refused under. Its presence is what marks an error as a plan refusal rather than a validation or permission failure.

Allowed:freeprobusinessenterprise
limitstring | integer

Which ceiling the call met, keyed by kind. A plan refusal (*_limit_exceeded) names the countable that ran out — version_history, routines_per_app, routines_per_owner, webhooks_per_app, webhooks_per_owner — and pairs it with max. An app_busy refusal names the bound it met and pairs it with observed and allowed; no plan lifts any of them. Two are per-app run bounds, concurrent_runs and runs_per_window; the third, render_token_refresh, is the page render asking for a new render-token secret, held by 10 per app per client IP and 60 per app across every address in a 60 second window, and observed/allowed name whichever of those two refused. An edits_too_large rejection carries the numeric cap that was exceeded instead of a name.

rungstring

On an app_busy refusal, how hard the app's own refusal ladder is refusing: throttled is the plain ceiling breach above; parked means a caller kept calling inside a wait it was given, so the server mints its own Retry-After (doubling from 60s to a 300s cap) and refuses until a quiet window passes even once the ceiling clears; closed is the same, plus the response closes the connection. Absent on every other kind.

Allowed:throttledparkedclosed
maxinteger

On a *_limit_exceeded refusal, that limit's number on the caller's plan.

min 0
featurestring

On a plan_required refusal, the capability the plan does not include. Never paired with limit / max.

Allowed:custom_domainwatermark_removalteams
upgradeUrlstring<uri>

Absolute link to the owner's own billing page, where the refusal is lifted — the team's page for a team-owned app. Omitted only when the owner has no handle and therefore no settings page of their own, so treat it as optional and fall back to your own copy. MCP appends it as an Upgrade: <url> last line; the CLI prints it under the message.

recoveryRecoveryPair | RecoveryRetry | RecoveryShrink | RecoveryRefetch | RecoveryFixRouteLabel | RecoveryRename | RecoveryMigrateContract

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 revision_mismatch / invalid_if_match / 428 precondition_required, fix_route_label for 403 forbidden_write, 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 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.

RecoveryRename
kindstringrequired
Allowed:rename
instructionsstringrequired
RecoveryMigrateContract
kindstringrequired
Allowed:migrate_contract
instructionsstringrequired
409

Idempotency conflict or frozen manifest identity conflict.

okbooleanrequired
Allowed:false
errorobjectrequired
Show properties
kindstringrequired

Stable enum-shaped error key. Branch on this for recovery flows.

Allowed:not_foundunknown_operationoperation_not_foundmethod_not_allowedoperation_failedapp_busyvisitor_rate_limitedservice_busyunauthorizedsign_in_requiredtoken_expiredforbiddenaccount_not_joinedforbidden_writestored_value_too_largestorage_fullinvalid_moduleinvalid_uiunpinned_cdn_scriptinvalid_route_schemainvalid_routes_shapecontract_migration_requiredinvalid_manifest_schemainvalid_manifest_exportsinvalid_requestinvalid_inputinvalid_outputunexpected_keyscapability_deniedcapability_errorexecutor_errorinvalid_call_shapealready_claimedmanifest_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_requiredexpected_revision_requiredidempotency_key_requiredidempotency_conflictcontract_not_enabledapp_build_capacityapp_build_failedapp_build_expiredapp_build_authority_lostapp_build_unavailableapp_build_unsupportedversion_history_unavailableversion_not_restorablerevision_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_reachedinvite_sender_blockedread_onlyhandle_takeninvalid_handlename_takeninvalid_domaindomain_takendomain_existsdomain_provider_unavailableapp_unclaimedlink_cap_reachedgrantee_is_ownergrantee_is_selfgrantee_already_has_accessalready_invitedroutine_limit_exceededinvalid_intervalop_requires_inputduplicate_routinewebhook_limit_exceededduplicate_webhookwebhook_disabledversion_limit_exceededplan_requiredstorage_limit_exceededcopy_data_forbiddencopy_data_too_large
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 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_claim
planstring

Present on every plan refusal: the tier the caller was refused under. Its presence is what marks an error as a plan refusal rather than a validation or permission failure.

Allowed:freeprobusinessenterprise
limitstring | integer

Which ceiling the call met, keyed by kind. A plan refusal (*_limit_exceeded) names the countable that ran out — version_history, routines_per_app, routines_per_owner, webhooks_per_app, webhooks_per_owner — and pairs it with max. An app_busy refusal names the bound it met and pairs it with observed and allowed; no plan lifts any of them. Two are per-app run bounds, concurrent_runs and runs_per_window; the third, render_token_refresh, is the page render asking for a new render-token secret, held by 10 per app per client IP and 60 per app across every address in a 60 second window, and observed/allowed name whichever of those two refused. An edits_too_large rejection carries the numeric cap that was exceeded instead of a name.

rungstring

On an app_busy refusal, how hard the app's own refusal ladder is refusing: throttled is the plain ceiling breach above; parked means a caller kept calling inside a wait it was given, so the server mints its own Retry-After (doubling from 60s to a 300s cap) and refuses until a quiet window passes even once the ceiling clears; closed is the same, plus the response closes the connection. Absent on every other kind.

Allowed:throttledparkedclosed
maxinteger

On a *_limit_exceeded refusal, that limit's number on the caller's plan.

min 0
featurestring

On a plan_required refusal, the capability the plan does not include. Never paired with limit / max.

Allowed:custom_domainwatermark_removalteams
upgradeUrlstring<uri>

Absolute link to the owner's own billing page, where the refusal is lifted — the team's page for a team-owned app. Omitted only when the owner has no handle and therefore no settings page of their own, so treat it as optional and fall back to your own copy. MCP appends it as an Upgrade: <url> last line; the CLI prints it under the message.

recoveryRecoveryPair | RecoveryRetry | RecoveryShrink | RecoveryRefetch | RecoveryFixRouteLabel | RecoveryRename | RecoveryMigrateContract

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 revision_mismatch / invalid_if_match / 428 precondition_required, fix_route_label for 403 forbidden_write, 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 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.

RecoveryRename
kindstringrequired
Allowed:rename
instructionsstringrequired
RecoveryMigrateContract
kindstringrequired
Allowed:migrate_contract
instructionsstringrequired
412

The desired source revision changed.

okbooleanrequired
Allowed:false
errorobjectrequired
Show properties
kindstringrequired

Stable enum-shaped error key. Branch on this for recovery flows.

Allowed:not_foundunknown_operationoperation_not_foundmethod_not_allowedoperation_failedapp_busyvisitor_rate_limitedservice_busyunauthorizedsign_in_requiredtoken_expiredforbiddenaccount_not_joinedforbidden_writestored_value_too_largestorage_fullinvalid_moduleinvalid_uiunpinned_cdn_scriptinvalid_route_schemainvalid_routes_shapecontract_migration_requiredinvalid_manifest_schemainvalid_manifest_exportsinvalid_requestinvalid_inputinvalid_outputunexpected_keyscapability_deniedcapability_errorexecutor_errorinvalid_call_shapealready_claimedmanifest_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_requiredexpected_revision_requiredidempotency_key_requiredidempotency_conflictcontract_not_enabledapp_build_capacityapp_build_failedapp_build_expiredapp_build_authority_lostapp_build_unavailableapp_build_unsupportedversion_history_unavailableversion_not_restorablerevision_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_reachedinvite_sender_blockedread_onlyhandle_takeninvalid_handlename_takeninvalid_domaindomain_takendomain_existsdomain_provider_unavailableapp_unclaimedlink_cap_reachedgrantee_is_ownergrantee_is_selfgrantee_already_has_accessalready_invitedroutine_limit_exceededinvalid_intervalop_requires_inputduplicate_routinewebhook_limit_exceededduplicate_webhookwebhook_disabledversion_limit_exceededplan_requiredstorage_limit_exceededcopy_data_forbiddencopy_data_too_large
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 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_claim
planstring

Present on every plan refusal: the tier the caller was refused under. Its presence is what marks an error as a plan refusal rather than a validation or permission failure.

Allowed:freeprobusinessenterprise
limitstring | integer

Which ceiling the call met, keyed by kind. A plan refusal (*_limit_exceeded) names the countable that ran out — version_history, routines_per_app, routines_per_owner, webhooks_per_app, webhooks_per_owner — and pairs it with max. An app_busy refusal names the bound it met and pairs it with observed and allowed; no plan lifts any of them. Two are per-app run bounds, concurrent_runs and runs_per_window; the third, render_token_refresh, is the page render asking for a new render-token secret, held by 10 per app per client IP and 60 per app across every address in a 60 second window, and observed/allowed name whichever of those two refused. An edits_too_large rejection carries the numeric cap that was exceeded instead of a name.

rungstring

On an app_busy refusal, how hard the app's own refusal ladder is refusing: throttled is the plain ceiling breach above; parked means a caller kept calling inside a wait it was given, so the server mints its own Retry-After (doubling from 60s to a 300s cap) and refuses until a quiet window passes even once the ceiling clears; closed is the same, plus the response closes the connection. Absent on every other kind.

Allowed:throttledparkedclosed
maxinteger

On a *_limit_exceeded refusal, that limit's number on the caller's plan.

min 0
featurestring

On a plan_required refusal, the capability the plan does not include. Never paired with limit / max.

Allowed:custom_domainwatermark_removalteams
upgradeUrlstring<uri>

Absolute link to the owner's own billing page, where the refusal is lifted — the team's page for a team-owned app. Omitted only when the owner has no handle and therefore no settings page of their own, so treat it as optional and fall back to your own copy. MCP appends it as an Upgrade: <url> last line; the CLI prints it under the message.

recoveryRecoveryPair | RecoveryRetry | RecoveryShrink | RecoveryRefetch | RecoveryFixRouteLabel | RecoveryRename | RecoveryMigrateContract

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 revision_mismatch / invalid_if_match / 428 precondition_required, fix_route_label for 403 forbidden_write, 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 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.

RecoveryRename
kindstringrequired
Allowed:rename
instructionsstringrequired
RecoveryMigrateContract
kindstringrequired
Allowed:migrate_contract
instructionsstringrequired
413

Source exceeds the contract limits.

okbooleanrequired
Allowed:false
errorobjectrequired
Show properties
kindstringrequired

Stable enum-shaped error key. Branch on this for recovery flows.

Allowed:not_foundunknown_operationoperation_not_foundmethod_not_allowedoperation_failedapp_busyvisitor_rate_limitedservice_busyunauthorizedsign_in_requiredtoken_expiredforbiddenaccount_not_joinedforbidden_writestored_value_too_largestorage_fullinvalid_moduleinvalid_uiunpinned_cdn_scriptinvalid_route_schemainvalid_routes_shapecontract_migration_requiredinvalid_manifest_schemainvalid_manifest_exportsinvalid_requestinvalid_inputinvalid_outputunexpected_keyscapability_deniedcapability_errorexecutor_errorinvalid_call_shapealready_claimedmanifest_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_requiredexpected_revision_requiredidempotency_key_requiredidempotency_conflictcontract_not_enabledapp_build_capacityapp_build_failedapp_build_expiredapp_build_authority_lostapp_build_unavailableapp_build_unsupportedversion_history_unavailableversion_not_restorablerevision_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_reachedinvite_sender_blockedread_onlyhandle_takeninvalid_handlename_takeninvalid_domaindomain_takendomain_existsdomain_provider_unavailableapp_unclaimedlink_cap_reachedgrantee_is_ownergrantee_is_selfgrantee_already_has_accessalready_invitedroutine_limit_exceededinvalid_intervalop_requires_inputduplicate_routinewebhook_limit_exceededduplicate_webhookwebhook_disabledversion_limit_exceededplan_requiredstorage_limit_exceededcopy_data_forbiddencopy_data_too_large
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 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_claim
planstring

Present on every plan refusal: the tier the caller was refused under. Its presence is what marks an error as a plan refusal rather than a validation or permission failure.

Allowed:freeprobusinessenterprise
limitstring | integer

Which ceiling the call met, keyed by kind. A plan refusal (*_limit_exceeded) names the countable that ran out — version_history, routines_per_app, routines_per_owner, webhooks_per_app, webhooks_per_owner — and pairs it with max. An app_busy refusal names the bound it met and pairs it with observed and allowed; no plan lifts any of them. Two are per-app run bounds, concurrent_runs and runs_per_window; the third, render_token_refresh, is the page render asking for a new render-token secret, held by 10 per app per client IP and 60 per app across every address in a 60 second window, and observed/allowed name whichever of those two refused. An edits_too_large rejection carries the numeric cap that was exceeded instead of a name.

rungstring

On an app_busy refusal, how hard the app's own refusal ladder is refusing: throttled is the plain ceiling breach above; parked means a caller kept calling inside a wait it was given, so the server mints its own Retry-After (doubling from 60s to a 300s cap) and refuses until a quiet window passes even once the ceiling clears; closed is the same, plus the response closes the connection. Absent on every other kind.

Allowed:throttledparkedclosed
maxinteger

On a *_limit_exceeded refusal, that limit's number on the caller's plan.

min 0
featurestring

On a plan_required refusal, the capability the plan does not include. Never paired with limit / max.

Allowed:custom_domainwatermark_removalteams
upgradeUrlstring<uri>

Absolute link to the owner's own billing page, where the refusal is lifted — the team's page for a team-owned app. Omitted only when the owner has no handle and therefore no settings page of their own, so treat it as optional and fall back to your own copy. MCP appends it as an Upgrade: <url> last line; the CLI prints it under the message.

recoveryRecoveryPair | RecoveryRetry | RecoveryShrink | RecoveryRefetch | RecoveryFixRouteLabel | RecoveryRename | RecoveryMigrateContract

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 revision_mismatch / invalid_if_match / 428 precondition_required, fix_route_label for 403 forbidden_write, 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 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.

RecoveryRename
kindstringrequired
Allowed:rename
instructionsstringrequired
RecoveryMigrateContract
kindstringrequired
Allowed:migrate_contract
instructionsstringrequired
422

Manifest, import/dependency mismatch, or explicit migration validation failed.

okbooleanrequired
Allowed:false
errorobjectrequired
Show properties
kindstringrequired

Stable enum-shaped error key. Branch on this for recovery flows.

Allowed:not_foundunknown_operationoperation_not_foundmethod_not_allowedoperation_failedapp_busyvisitor_rate_limitedservice_busyunauthorizedsign_in_requiredtoken_expiredforbiddenaccount_not_joinedforbidden_writestored_value_too_largestorage_fullinvalid_moduleinvalid_uiunpinned_cdn_scriptinvalid_route_schemainvalid_routes_shapecontract_migration_requiredinvalid_manifest_schemainvalid_manifest_exportsinvalid_requestinvalid_inputinvalid_outputunexpected_keyscapability_deniedcapability_errorexecutor_errorinvalid_call_shapealready_claimedmanifest_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_requiredexpected_revision_requiredidempotency_key_requiredidempotency_conflictcontract_not_enabledapp_build_capacityapp_build_failedapp_build_expiredapp_build_authority_lostapp_build_unavailableapp_build_unsupportedversion_history_unavailableversion_not_restorablerevision_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_reachedinvite_sender_blockedread_onlyhandle_takeninvalid_handlename_takeninvalid_domaindomain_takendomain_existsdomain_provider_unavailableapp_unclaimedlink_cap_reachedgrantee_is_ownergrantee_is_selfgrantee_already_has_accessalready_invitedroutine_limit_exceededinvalid_intervalop_requires_inputduplicate_routinewebhook_limit_exceededduplicate_webhookwebhook_disabledversion_limit_exceededplan_requiredstorage_limit_exceededcopy_data_forbiddencopy_data_too_large
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 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_claim
planstring

Present on every plan refusal: the tier the caller was refused under. Its presence is what marks an error as a plan refusal rather than a validation or permission failure.

Allowed:freeprobusinessenterprise
limitstring | integer

Which ceiling the call met, keyed by kind. A plan refusal (*_limit_exceeded) names the countable that ran out — version_history, routines_per_app, routines_per_owner, webhooks_per_app, webhooks_per_owner — and pairs it with max. An app_busy refusal names the bound it met and pairs it with observed and allowed; no plan lifts any of them. Two are per-app run bounds, concurrent_runs and runs_per_window; the third, render_token_refresh, is the page render asking for a new render-token secret, held by 10 per app per client IP and 60 per app across every address in a 60 second window, and observed/allowed name whichever of those two refused. An edits_too_large rejection carries the numeric cap that was exceeded instead of a name.

rungstring

On an app_busy refusal, how hard the app's own refusal ladder is refusing: throttled is the plain ceiling breach above; parked means a caller kept calling inside a wait it was given, so the server mints its own Retry-After (doubling from 60s to a 300s cap) and refuses until a quiet window passes even once the ceiling clears; closed is the same, plus the response closes the connection. Absent on every other kind.

Allowed:throttledparkedclosed
maxinteger

On a *_limit_exceeded refusal, that limit's number on the caller's plan.

min 0
featurestring

On a plan_required refusal, the capability the plan does not include. Never paired with limit / max.

Allowed:custom_domainwatermark_removalteams
upgradeUrlstring<uri>

Absolute link to the owner's own billing page, where the refusal is lifted — the team's page for a team-owned app. Omitted only when the owner has no handle and therefore no settings page of their own, so treat it as optional and fall back to your own copy. MCP appends it as an Upgrade: <url> last line; the CLI prints it under the message.

recoveryRecoveryPair | RecoveryRetry | RecoveryShrink | RecoveryRefetch | RecoveryFixRouteLabel | RecoveryRename | RecoveryMigrateContract

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 revision_mismatch / invalid_if_match / 428 precondition_required, fix_route_label for 403 forbidden_write, 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 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.

RecoveryRename
kindstringrequired
Allowed:rename
instructionsstringrequired
RecoveryMigrateContract
kindstringrequired
Allowed:migrate_contract
instructionsstringrequired
428

A required desired-source revision is missing.

okbooleanrequired
Allowed:false
errorobjectrequired
Show properties
kindstringrequired

Stable enum-shaped error key. Branch on this for recovery flows.

Allowed:not_foundunknown_operationoperation_not_foundmethod_not_allowedoperation_failedapp_busyvisitor_rate_limitedservice_busyunauthorizedsign_in_requiredtoken_expiredforbiddenaccount_not_joinedforbidden_writestored_value_too_largestorage_fullinvalid_moduleinvalid_uiunpinned_cdn_scriptinvalid_route_schemainvalid_routes_shapecontract_migration_requiredinvalid_manifest_schemainvalid_manifest_exportsinvalid_requestinvalid_inputinvalid_outputunexpected_keyscapability_deniedcapability_errorexecutor_errorinvalid_call_shapealready_claimedmanifest_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_requiredexpected_revision_requiredidempotency_key_requiredidempotency_conflictcontract_not_enabledapp_build_capacityapp_build_failedapp_build_expiredapp_build_authority_lostapp_build_unavailableapp_build_unsupportedversion_history_unavailableversion_not_restorablerevision_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_reachedinvite_sender_blockedread_onlyhandle_takeninvalid_handlename_takeninvalid_domaindomain_takendomain_existsdomain_provider_unavailableapp_unclaimedlink_cap_reachedgrantee_is_ownergrantee_is_selfgrantee_already_has_accessalready_invitedroutine_limit_exceededinvalid_intervalop_requires_inputduplicate_routinewebhook_limit_exceededduplicate_webhookwebhook_disabledversion_limit_exceededplan_requiredstorage_limit_exceededcopy_data_forbiddencopy_data_too_large
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 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_claim
planstring

Present on every plan refusal: the tier the caller was refused under. Its presence is what marks an error as a plan refusal rather than a validation or permission failure.

Allowed:freeprobusinessenterprise
limitstring | integer

Which ceiling the call met, keyed by kind. A plan refusal (*_limit_exceeded) names the countable that ran out — version_history, routines_per_app, routines_per_owner, webhooks_per_app, webhooks_per_owner — and pairs it with max. An app_busy refusal names the bound it met and pairs it with observed and allowed; no plan lifts any of them. Two are per-app run bounds, concurrent_runs and runs_per_window; the third, render_token_refresh, is the page render asking for a new render-token secret, held by 10 per app per client IP and 60 per app across every address in a 60 second window, and observed/allowed name whichever of those two refused. An edits_too_large rejection carries the numeric cap that was exceeded instead of a name.

rungstring

On an app_busy refusal, how hard the app's own refusal ladder is refusing: throttled is the plain ceiling breach above; parked means a caller kept calling inside a wait it was given, so the server mints its own Retry-After (doubling from 60s to a 300s cap) and refuses until a quiet window passes even once the ceiling clears; closed is the same, plus the response closes the connection. Absent on every other kind.

Allowed:throttledparkedclosed
maxinteger

On a *_limit_exceeded refusal, that limit's number on the caller's plan.

min 0
featurestring

On a plan_required refusal, the capability the plan does not include. Never paired with limit / max.

Allowed:custom_domainwatermark_removalteams
upgradeUrlstring<uri>

Absolute link to the owner's own billing page, where the refusal is lifted — the team's page for a team-owned app. Omitted only when the owner has no handle and therefore no settings page of their own, so treat it as optional and fall back to your own copy. MCP appends it as an Upgrade: <url> last line; the CLI prints it under the message.

recoveryRecoveryPair | RecoveryRetry | RecoveryShrink | RecoveryRefetch | RecoveryFixRouteLabel | RecoveryRename | RecoveryMigrateContract

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 revision_mismatch / invalid_if_match / 428 precondition_required, fix_route_label for 403 forbidden_write, 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 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.

RecoveryRename
kindstringrequired
Allowed:rename
instructionsstringrequired
RecoveryMigrateContract
kindstringrequired
Allowed:migrate_contract
instructionsstringrequired
429

Build capacity is full (app_build_capacity). Retry-After gives the retry delay.

okbooleanrequired
Allowed:false
errorobjectrequired
Show properties
kindstringrequired

Stable enum-shaped error key. Branch on this for recovery flows.

Allowed:not_foundunknown_operationoperation_not_foundmethod_not_allowedoperation_failedapp_busyvisitor_rate_limitedservice_busyunauthorizedsign_in_requiredtoken_expiredforbiddenaccount_not_joinedforbidden_writestored_value_too_largestorage_fullinvalid_moduleinvalid_uiunpinned_cdn_scriptinvalid_route_schemainvalid_routes_shapecontract_migration_requiredinvalid_manifest_schemainvalid_manifest_exportsinvalid_requestinvalid_inputinvalid_outputunexpected_keyscapability_deniedcapability_errorexecutor_errorinvalid_call_shapealready_claimedmanifest_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_requiredexpected_revision_requiredidempotency_key_requiredidempotency_conflictcontract_not_enabledapp_build_capacityapp_build_failedapp_build_expiredapp_build_authority_lostapp_build_unavailableapp_build_unsupportedversion_history_unavailableversion_not_restorablerevision_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_reachedinvite_sender_blockedread_onlyhandle_takeninvalid_handlename_takeninvalid_domaindomain_takendomain_existsdomain_provider_unavailableapp_unclaimedlink_cap_reachedgrantee_is_ownergrantee_is_selfgrantee_already_has_accessalready_invitedroutine_limit_exceededinvalid_intervalop_requires_inputduplicate_routinewebhook_limit_exceededduplicate_webhookwebhook_disabledversion_limit_exceededplan_requiredstorage_limit_exceededcopy_data_forbiddencopy_data_too_large
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 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_claim
planstring

Present on every plan refusal: the tier the caller was refused under. Its presence is what marks an error as a plan refusal rather than a validation or permission failure.

Allowed:freeprobusinessenterprise
limitstring | integer

Which ceiling the call met, keyed by kind. A plan refusal (*_limit_exceeded) names the countable that ran out — version_history, routines_per_app, routines_per_owner, webhooks_per_app, webhooks_per_owner — and pairs it with max. An app_busy refusal names the bound it met and pairs it with observed and allowed; no plan lifts any of them. Two are per-app run bounds, concurrent_runs and runs_per_window; the third, render_token_refresh, is the page render asking for a new render-token secret, held by 10 per app per client IP and 60 per app across every address in a 60 second window, and observed/allowed name whichever of those two refused. An edits_too_large rejection carries the numeric cap that was exceeded instead of a name.

rungstring

On an app_busy refusal, how hard the app's own refusal ladder is refusing: throttled is the plain ceiling breach above; parked means a caller kept calling inside a wait it was given, so the server mints its own Retry-After (doubling from 60s to a 300s cap) and refuses until a quiet window passes even once the ceiling clears; closed is the same, plus the response closes the connection. Absent on every other kind.

Allowed:throttledparkedclosed
maxinteger

On a *_limit_exceeded refusal, that limit's number on the caller's plan.

min 0
featurestring

On a plan_required refusal, the capability the plan does not include. Never paired with limit / max.

Allowed:custom_domainwatermark_removalteams
upgradeUrlstring<uri>

Absolute link to the owner's own billing page, where the refusal is lifted — the team's page for a team-owned app. Omitted only when the owner has no handle and therefore no settings page of their own, so treat it as optional and fall back to your own copy. MCP appends it as an Upgrade: <url> last line; the CLI prints it under the message.

recoveryRecoveryPair | RecoveryRetry | RecoveryShrink | RecoveryRefetch | RecoveryFixRouteLabel | RecoveryRename | RecoveryMigrateContract

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 revision_mismatch / invalid_if_match / 428 precondition_required, fix_route_label for 403 forbidden_write, 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 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.

RecoveryRename
kindstringrequired
Allowed:rename
instructionsstringrequired
RecoveryMigrateContract
kindstringrequired
Allowed:migrate_contract
instructionsstringrequired
Try it
Server
Authorization (optional)
Parameters
Bodyapplication/json
Request
curl -X POST 'https://charm.ing/app' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "module": "string",
  "ui": "string",
  "styles": "string",
  "description": "string",
  "migrate_contract": true,
  "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"
  ]
}