> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cskn.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Quotas

> Show how much of a limit the user has used, and handle being refused for going over it.

Some apps limit what one user can keep or do: how many photos they store, how many posts they make a day. Each limit is a **quota** with a key in the form `app.name`, for example `photos.items` or `shout.posts`.

The limit you read already includes the user's plan and anything granted to them on top. Show it as you receive it. Never hard-code a number in your app: the same quota can be 100 for one user and 500 for another.

```tsx theme={null}
import { api, useQuota, useRefreshQuotas, quotaExceeded } from '@cskn/sdk';

function NewPageButton() {
  const { data: quota } = useQuota('pages.pages');
  const refresh = useRefreshQuotas();
  const full = quota?.limit != null && quota.used != null && quota.used >= quota.limit;

  const create = async () => {
    try {
      await api.post('/apps/pages', { title: 'Untitled' });
    } catch (error) {
      const refused = quotaExceeded(error);
      if (refused) alert(`You can have up to ${refused.limit} pages.`);
    } finally {
      void refresh();
    }
  };

  return (
    <>
      <button onClick={create} disabled={full}>New page</button>
      {quota?.limit != null && <small>{quota.used} of {quota.limit} pages</small>}
    </>
  );
}
```

## useQuota

Reads one quota by key.

<ParamField path="key" type="string" required>
  The quota's key, `app.name`.
</ParamField>

<ParamField path="options.parentId" type="string">
  For a quota that applies to one container rather than the whole account, such as the items in one list, the container's id.
</ParamField>

<ParamField path="options.enabled" type="boolean" default="true">
  Set to `false` to hold the request.
</ParamField>

<ResponseField name="limit" type="number | null">
  The user's limit. `null` means no limit.
</ResponseField>

<ResponseField name="used / remaining" type="number | null">
  How much is used and how much is left. `null` when the quota is per container and you didn't pass `parentId`.
</ResponseField>

<ResponseField name="unit" type="'count' | 'bytes' | 'seconds' | 'credits'">
  What the numbers count. Format bytes and seconds before showing them.
</ResponseField>

<ResponseField name="kind" type="'stock' | 'period'">
  `stock` is what the user holds; deleting something frees room. `period` is what they use per day, week or month.
</ResponseField>

<ResponseField name="resetsAt" type="string | null">
  For a `period` quota, the ISO time it starts over. Daily quotas reset at the user's own midnight. `null` for `stock` quotas.
</ResponseField>

<ResponseField name="upgradable" type="boolean">
  `true` when a paid plan would raise this limit. Use it to decide whether to mention upgrading.
</ResponseField>

Usage changes as the user creates and deletes things, sometimes from another device. Call `useRefreshQuotas()` after your app changes what a quota counts, and after a refusal.

## useQuotas

`useQuotas()` returns `{ plan, quotas }`: the user's plan (`free`, `plus` or `pro`) and every quota your app can read. Use it for a usage screen.

Your app sees its own quotas and the pools that all apps share, such as [AI credits](/sdk/ai-credits). It never sees another app's quotas. An app with no quotas of its own gets only the shared pools.

## Handling a refusal

A request over a quota fails, and its body carries a `quota` object with `code: 'QUOTA_EXCEEDED'`. `quotaExceeded(error)` returns that object, or `null` for any other error. It has the same fields as `useQuota`, plus the `key` that refused.

Read the refusal with `quotaExceeded` rather than from the HTTP status or the top-level `code`. Those differ from app to app; the `quota` object is the same everywhere.

| What you get | What to tell the user |
| - | - |
| `quotaExceeded(error)` returns a quota of kind `stock` | They're at their limit. Deleting something frees room. |
| `quotaExceeded(error)` returns a quota of kind `period` | They're out for now. It comes back at `resetsAt`. |
| `aiCreditsExhausted(error)` returns a value | The month's AI credits are spent. See [AI credits](/sdk/ai-credits). |

Running out of AI credits answers **402** and is read with `aiCreditsExhausted`, not `quotaExceeded`.

Without React, use `getQuota(key, options)` and `getQuotas()`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.