> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.morphiaone.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.morphiaone.ai/_mcp/server.

# Rate limits

## The policies

Three limits apply to the calls that spend them. Each is per environment: every key of
your live environment shares the live limits, and your test environment has its own.

| Policy   | Spent by                    | Window  | Resets                             |
| -------- | --------------------------- | ------- | ---------------------------------- |
| `submit` | each job submission (`202`) | 60 s    | continuously, as the window passes |
| `upload` | each asset upload (`201`)   | 60 s    | continuously, as the window passes |
| `daily`  | each admitted job           | 86400 s | at 00:00 UTC                       |

The numbers depend on your plan. A limit can change while you are using the API, for
example when your plan changes; the new value applies within about a minute. Reads —
polling a job, fetching a result, usage and balance — are not rate limited and carry
no rate-limit headers.

A refusal is a `429` with one of these codes:

```text
RATE_LIMITED                 the submit or upload policy is spent; retry after the reset
QUOTA_EXCEEDED               the daily policy is spent; it resets at 00:00 UTC
TENANT_QUEUE_LIMIT_EXCEEDED  too many of your jobs are waiting; retry after Retry-After
```

## The headers

> **Note**
>
> Rate-limit headers are turned on per account. Until they are on for yours, responses
> carry none of them, and every limit below is still enforced the same way. Handle `429`
> and `Retry-After` whether or not you see the headers.

A response that spent a policy carries the IETF structured pair
(draft-ietf-httpapi-ratelimit-headers): `RateLimit-Policy` names each policy's quota
`q` per window `w` seconds, and `RateLimit` names what remains, `r`, and the seconds
until the policy is whole again, `t`.

```http
HTTP/1.1 202 Accepted
RateLimit-Policy: "submit";q=60;w=60, "daily";q=1000;w=86400
RateLimit: "submit";r=59;t=1, "daily";r=412;t=30791
RateLimit-Limit: 60
RateLimit-Remaining: 59
RateLimit-Reset: 1
```

The `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` headers carry the
same reading for clients that only understand those. They describe one policy only:
the one with the least remaining (on a tie, the later reset). Prefer the structured
pair.

A refusal carries the same headers, with `r=0` for the policy that refused it:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 4
RateLimit-Policy: "submit";q=60;w=60
RateLimit: "submit";r=0;t=4
```

The `daily` item appears only when your plan has a daily quota. Its `r` is advisory: it
is read just after your job was admitted, so a submission made at the same moment can
make it lag by one. The limit itself is enforced exactly.

## How the per-minute limits refill

The `submit` and `upload` policies are either a fixed window, which refills all at
once when the minute ends, or a token bucket (GCRA), where each call spends one of `q`
tokens and tokens come back steadily, one every `w / q` seconds. Either way `t` is the
honest wait: the rest of the minute for a fixed window, the next token for the bucket.

## What is not advertised

Two limits are enforced without headers: how many of your jobs may wait in the queue,
and how many may run at once. Both change as jobs start and finish, so a number in one
response would be stale by the next. A full queue answers `TENANT_QUEUE_LIMIT_EXCEEDED`
with `Retry-After`; a concurrency limit only delays when a queued job starts and never
refuses a submission.

## In the SDKs

Both SDKs parse the headers, so you do not have to. Where the headers are off, the
readings below are empty.

**`TypeScript`**

```ts title="TypeScript"
import { MorphiaOne, parseRateLimit } from '@morphiaone/sdk';

const client = new MorphiaOne({ apiKey, baseUrl: 'https://api.morphiaone.ai' });
await client.skin.analyze({ assetId, concerns: ['acne'] });
console.log(client.lastRateLimit); // [{ policy, limit, remaining, reset, window }, ...]
```

**`Python`**

```python title="Python"
from morphiaone import MorphiaOne, RateLimitError

client = MorphiaOne(api_key=api_key, base_url="https://api.morphiaone.ai")
job = client.skin.analyze(asset.id, ["acne"])
for policy in client.last_rate_limit:
    print(policy.policy, policy.remaining, policy.reset)

try:
    client.skin.analyze(asset.id, ["acne"])
except RateLimitError as error:
    print(error.code, error.rate_limit)
```

`last_rate_limit` / `lastRateLimit` holds what the last submission or upload advertised;
polling does not clear it. An error carries its own as `error.rate_limit` /
`error.rateLimit`. When a `RATE_LIMITED` refusal has no `Retry-After`, the SDKs wait the
refusing policy's `t` before their retry. They never retry `QUOTA_EXCEEDED`. See
[Retries and backoff](/concepts/retries).