Skip to content
Esc
↑↓navigate↵open⌘Jpreview
On this page

Pricing and limits

What each Charming plan costs and includes, plus size caps, rate limits, and token lifetimes for Charming apps.

The first anonymous app needs no signup and lasts 7 days. Claim it to keep it. There is no enforced app cap per account today. The OpenAPI specification is the machine-readable source for current limits.

Tiers at a glance

Tier Signup App persistence
Anonymous Not required 7 days from creation
Claimed Required Indefinite

See Authentication for token scope, claim, recovery, and revocation.

Plans

A claimed app belongs to an owner, an account or a team, and the owner’s plan sets the numbers below. Every owner starts on Free. Pro is for one person; Business is for a team and is billed per seat. The pricing page compares the plans side by side.

Free Pro Business
Price $0 a month $8 a month $14 per seat a month, three seats minimum
Saved versions per app 10 50 200
Routines per app 3 10 25
Routines in total 25 100 100 per seat
Webhooks per app 3 10 25
Webhooks in total 25 100 100 per seat
Webhook body kept per delivery 16 KB 64 KB 64 KB
Runs a month 1,000,000 5,000,000 5,000,000 per seat
Storage 1 GB 25 GB 25 GB per seat
Activity history 24 hours 30 days 90 days
Usage history 30 days 12 months 12 months
Remove the Charming mark No Yes Yes
Custom domains No Yes Yes

A per-seat number is multiplied by the seats a team is billed for, and a Business team is always billed for at least three. Enterprise plans set their numbers per contract. The owner’s billing page, in Account settings or the team’s settings, is where a plan is bought or changed.

Webhook body kept per delivery sizes a record, not a delivery. A Webhook’s delivery record keeps that much of the request body and that much of the response body, and drops the rest. The op still gets the whole request, and the sender the whole response.

A capability a plan does not include refuses with plan_required. Adding a Routine or a Webhook past the plan’s cap refuses with routine_limit_exceeded or webhook_limit_exceeded, and restoring a version outside its saved versions refuses with version_limit_exceeded. See Plan refusals.

Per-account limits

Resource Limit
Apps per user No enforced cap today (fair use)
list_apps default page size 50
list_apps max page size 100
Routines per owner 25 on Free, more on paid plans (routine_limit_exceeded, 429)
Webhooks per owner 25 on Free, more on paid plans (webhook_limit_exceeded, 429)

Listing only returns the caller’s apps (owned + collaborator); paging uses an opaque cursor.

The Routines and Webhooks caps count across all the apps one owner holds, whether that owner is you or a team. An unclaimed app has no owner, so only the per-app cap bounds it.

Per-app limits

Resource Cap Error kind on breach
module source 256 KiB module_too_large (413)
ui source 256 KiB ui_too_large (413)
description 500 JS chars description_too_long (400)
starter_prompt 2000 JS chars invalid_input (400)
Feedback text body 16 KiB chars text_too_large (413)
Feedback structuredData (serialized) 64 KiB structured_data_too_large (413)
Feedback wire body 96 KiB payload_too_large (413)
Edits batch (PATCH /app/{id}/source) per-string + per-batch cap edits_too_large (413)
Diag report body (POST /app/{id}/diag/report) 16 KiB payload_too_large (413)
Load-error report body 2 KiB payload_too_large (413)
Per-app asset size 10 MiB asset_too_large (413)
Per-app asset count 50 asset_count_exceeded (413)
Per-app asset total 100 MiB asset_quota_exceeded (413)
env.storage, one value 10 MiB stored_value_too_large (413)
env.storage, all values 1 GiB storage_full (413)
Data one copy_app with include_data carries, stored values and assets together 24 MiB copy_data_too_large (413)
env.storage, one visitor’s own store on a custom domain 100 MiB storage_full (413)
Per-app secret value 8 KiB secret_too_large (400)
Per-app secret name uniqueness one row per (scope, manifestId, name) secret_already_exists (409)
Per-app diag ring buffer 50 entries (oldest rolls off) None
Durable activity events Kept 90 days, kind-capped; readable for the plan’s window (see Plans) None
Routines per app 3 on Free, more on paid plans routine_limit_exceeded (429)
Webhooks per app 3 on Free, more on paid plans webhook_limit_exceeded (429)
Version History window 10 revisions on Free, more on paid plans version_limit_exceeded (429) on restore
Routine interval floor hourly invalid_interval (400)
Routine auto-disable 5 consecutive tick failures None (see Routines)

Both env.storage caps apply. Delete or shrink a key to make room. Everyone using an app shares one store, the owner’s; anonymous visitors on a custom domain get their own.

Plan refusals

Some limits belong to the owner’s plan rather than to the app. They all refuse with one shape, so a caller reads the same fields wherever it meets one.

Situation Error kind Status
A countable ran out routine_limit_exceeded, webhook_limit_exceeded, version_limit_exceeded, storage_limit_exceeded 429
The plan does not include the capability plan_required 403
Field Meaning
error.plan The tier the caller was refused under. Its presence is what marks an error as a plan refusal.
error.limit and error.max Which countable ran out and its number on that plan. Present on a *_limit_exceeded refusal. error.limit also appears on an app_busy refusal with a different vocabulary, naming a per-app run bound that no plan lifts.
error.feature The capability the plan does not include. Present on a plan_required refusal.
error.upgradeUrl Absolute link to the owner’s billing page, where the refusal is lifted. A team-owned app links to the team’s page.

upgradeUrl is omitted when the owner has no handle and therefore no settings page of their own, so treat it as optional. MCP repeats it as an Upgrade: <url> last line in the error text, and the CLI prints it under the message.

A refusal is not a transient failure. Nothing about the request was wrong, so retrying it returns the same answer until the plan changes or the count drops.

Restoring a version older than the plan’s History window is refused with version_limit_exceeded. Inside the window, a revision that is missing or cannot be rebuilt stays version_not_restorable, which upgrading does not fix. Outside it the plan answers first, so a revision that is both out of reach and unbuildable reads as a plan refusal.

Usage allowances

Runs and storage count across every app an owner holds. A team’s apps count toward the team. Each plan’s allowance is in Plans. A month of Runs is the last 30 days, rolling. Activity history is what /activity and get_app’s recent issues can read.

A Run is an app load, an operation, or a Routine firing. Storage includes what visitors save in public apps. The Usage page, for the account and for each app, shows Runs by kind over 7 days, 30 days, or 12 months; ranges past the plan are locked.

At 80% the dashboard shows a notice. At 100% the owner, or a team’s admins, get one email, at most once every 30 days.

Storage past the allowance does not stop an app from saving. The one write it refuses is copying an app with its data: a copy whose data would put the caller over their allowance fails with storage_limit_exceeded and creates nothing.

Activity past the window is hidden, not deleted, and comes back on upgrade. Both activity endpoints return retention: the window in hours, the since cutoff used, and truncated, true when the window set the cutoff.

ESM build limits

Selected authenticated authors can use the ESM contract. It adds a 64 KiB combined module/UI source cap, 256 KiB styles cap, and 2 MiB compiled output cap per target. The build queue admits 16 pending operations overall and 3 per author, with 2 concurrent builds overall and 1 per author. Queue and attempt deadlines are 10 minutes each, within a 20-minute operation lifetime and at most 2 automatic attempts for transient failures.

Terminal input inspection lasts 7 days; operation results, exact locks, and idempotency records last 30 days. Active and historical completed builds remain while app revisions reference them. The public OpenAPI document exposes the current policy as x-charming-app-build-policy.

Network access

Backend requests must use HTTPS and declare each exact origin. Private, loopback, link-local, and cloud-metadata addresses are blocked, including after redirects. Sealed env.fetch responses are capped at 5 MiB. See the build reference for capabilities, permissions, and secret handling.

Rate limits

Token-bucket limiters, in-memory, per process. All windows are 60 seconds.

Route family Per (app, IP) Per app Per IP Per user
POST /app/{id}/diag/report 30/min 200/min None None
POST /app/{id}/load-error/report 30/min 200/min None None
POST /app/{id}/feedback 30/min 200/min None None
POST /app/{id}/assets / DELETE /app/{id}/assets/{key} throttled per app None None None
POST /api/pair/start None None 30/min None
POST /api/pair/poll None None 60/min None
POST /api/pair/approve None None None 20/min
POST /api/v1/hooks/{webhookId} None None 300/min None

A Webhook delivery that passes its secret check also takes from a second bucket of 120 a minute per Webhook.

A breach returns 429 with error.kind: "rate_limited" (or "too_many_requests" for asset writes).

Running an app

Every call that runs an app’s own handler is bounded, whichever surface it arrives on: POST /app/{id}/api/{op}, MCP query_app / mutate_app, Routine ticks, and Webhook deliveries all share one budget per app.

Bound Cap Meaning
Runs in flight at once 25 Simultaneous executions of this app
Runs started per 60 seconds 300 Rolling window, not a fixed reset

A breach returns 429 with error.kind: "app_busy". error.limit names which bound was hit (concurrent_runs, runs_per_window, or render_token_refresh), and error.execution is always not_started, so the call never reached the handler and retrying it is safe. The rolling-window breach also carries error.retry_after_seconds and a Retry-After header; the in-flight breach carries neither, because capacity frees when a run finishes rather than at a knowable time.

render_token_refresh is the page render asking for a new render-token secret. Two bounds hold it in a 60-second window: 10 per app per client IP, and 60 per app across every address. error.observed and error.allowed name whichever one refused, so back off against those numbers rather than assuming the wider bound. The per-(app, IP) one is met first in practice, and on a deployment that does not trust a proxy header every caller resolves to the same address, which puts all of them in one 10-rotation bucket.

Callers who are not members

An operation whose route declares callers (see Let visitors call an operation) has extra bounds for callers who are not members of the App. Members are never held to them.

Bound Cap Applies to
Calls per 60 seconds from one address 30 A caller who is not signed in, across the App’s operations
Calls per 60 seconds from one account, per operation 30 A signed-in caller who is not a member
Non-member runs in flight at once 12 Every non-member together
Non-member runs started per 60 seconds 150 Every non-member together

The last two are shares of the App’s own bounds above, so callers who are not members can never fill them and members always keep the rest. A non-member who arrives once the App has already reached one of its own bounds is refused the same way, as visitor_rate_limited, with error.limit naming the App’s own bound (concurrent_runs or runs_per_window) and its number; only a call refused before its caller is identified answers app_busy.

A breach returns 429 with error.kind: "visitor_rate_limited". error.limit names the bound (visitor_per_address, visitor_per_account, visitor_concurrent_runs, or visitor_runs_per_window), and error.execution is not_started, so retrying is safe. Every bound except the in-flight one also carries error.retry_after_seconds and a Retry-After header. A refused call is not a Run, does not count against the owner’s plan, and never moves the App down the refusal ladder. Every call a caller makes spends from their own bucket, including one the non-member shares then refuse, so retrying against a full share also runs down the caller’s 30. The activity timeline shows refused calls as one visitor_calls_refused row per 10-second window, with count and limit.

When the host itself is at capacity

The server holds a fixed pool of database connections, split so the health endpoint answers while app traffic saturates the rest. A call that cannot get a connection within one second returns 503 with error.kind: "service_busy" and Retry-After: 1. The refusal lands on one statement rather than on the whole call, so a call refused partway through may already have written part of its work. Repeat a read freely; after a write, read the result back before retrying.

This is not your app’s ceiling: app_busy says this app is doing too much, service_busy says the server is. Calls presenting a render token or no credential are refused before calls presenting a user or app token, so a signed-in owner keeps working while anonymous traffic sheds.

An app already at a ceiling is refused at the edge of the request, before its caller’s token is read. A caller holding no credential sees the same 429, and the refusal never spends the app’s budget: a slot is only taken by a call that passed authorization, so nobody can exhaust an app by naming its id.

Refusals are summarized rather than recorded one by one. The app’s activity timeline gets one api_proxy_result row per surface per 10-second window, carrying count (how many calls it stands for), limit (which ceiling refused them), and the first and last timestamps of the window.

An app that trips these is usually looping rather than popular. The most common cause is a refresh handler that triggers the change it is listening for.

Ignoring the wait

Every app_busy body carries error.rung, which says how hard the refusal is. A repeat from the same credential moves the app down a rung when it arrives inside the wait the ladder has already issued: the rolling-window bound’s own Retry-After, or, once a climb has minted one at parked or closed, that minted wait — whichever bound this particular refusal happens to name. Before anything is minted, the in-flight bound’s own first climb has no wait to arrive inside, so a repeat counts there when it lands within one second of the last refusal, at least 50 ms after it. A quiet 60 seconds moves the app back up one rung.

error.rung What the caller gets
throttled The ceiling breach above: the rolling window’s Retry-After, or none for the in-flight bound
parked A Retry-After the server mints, starting at 60 seconds and doubling per further refusal to a 300-second cap. The app is refused until a quiet window passes, even when its counters have drained
closed The same, plus Connection: close on the response

Only a call that passed authorization can move an app down a rung, and only when the same credential comes back inside the wait; an unauthenticated flood against an app id cannot park its owner’s app. A user token, an app token, and a visitor- or user-bound render token each key the ladder narrowly enough that two of those refused a second apart have ignored nothing between them. Two credentials are shared: an anonymous caller keys on its egress IP, one bucket for every anonymous request behind it (or a single bucket for all of them when the deployment has no trusted proxy in front), and the opaque render token minted for a public page is scoped to the app rather than to a reader, so every visitor to that page shares one key. A self-describing render token narrows that shared key to the reader who holds it.

Rate-limit response headers

Every rate-limited route emits the draft-IETF RateLimit-* headers (plus their legacy X-RateLimit-* aliases for older agent tooling) on both success and 429 responses so a client can project headroom without busy-polling:

Header Value When
RateLimit-Limit Capacity of the most-restrictive bucket gating this request (e.g. 30). every response
RateLimit-Remaining Floored whole tokens left in that bucket. 0 on the response that exhausts it. every response
RateLimit-Reset Integer seconds until the next whole token. 0 while remaining > 0. every response
X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset Legacy aliases with the same values. every response
Retry-After Integer seconds the client should wait before retrying. Equals RateLimit-Reset on every 429. 429 only

The numbers reflect the most-restrictive bucket among all that gate the request (e.g. (app, IP) vs (app) for diag/feedback/load-error). The Retry-After value lets generic HTTP clients back off without parsing the body.

Token TTLs

Token Default TTL Notes
chrm_app_* (anonymous create) Until claimed or app reaped (7 days unclaimed) Authorises one app; revoked on claim
chrm_user_* (POST /api/token or Connections settings) 7 days Personal access token; a signed-in session can pick 1 to 365 days with expiresInDays, a token minting another gets 7. Rotate it without a browser by minting a new one and revoking the old
chrm_user_* (paired via POST /api/pair/start) 30 days Issued when a user approves a device-pairing grant
chrm_render_* (server-minted at render) 24 hours, inside a 7-day secret (rotates on re-render; previous hash kept live for a ~15-minute grace window) Bound to one app id; not user-mintable. A page open past 24 hours gets token_expired; reloading it mints a fresh token
Device-pairing grant (device_code + user_code) 10 minutes (hard cap 60 minutes with active polling) RFC 8628-shaped; user_code displayed to human

chrm_user_* token revocation is immediate (DELETE /api/token/<id>), including for the token that makes the call. The authentication reference has the list call, the browser path, and the rotation order.

What is NOT metered today

Beyond the usage allowances:

  • App create / update calls have no per-minute write limit on POST /app or PUT /app/{id}. ESM builds have the queue and concurrency caps above.
  • App reads (GET /app/{id}, GET /app/{id}/source, GET /app/{id}/describe, GET /app/{id}/agent.json)

The route limits cover paths an anonymous caller can hit: diagnostics, feedback, asset writes, pairing, and Webhook deliveries. Authenticated PUT and PATCH writes use the bearer token instead of a per-minute counter.

When numbers change

Fetch the OpenAPI specification for the live values.

Was this page helpful?