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 /apporPUT /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.