<!-- x-generated: Generated by the Hyperscale artifact pipeline; do not edit by hand -->

# Operational limits

The platform-wide operating envelope: how requests are rate limited, how large they can be, how lists page, how a reserved transfer's deadline is enforced, and how the surface is versioned. Webhook signing and verification live in the quickstart's webhook section.

## Rate limits

Authenticated `/v1/*` traffic normally uses a fixed 60-second window against two budgets at once: per credential (default 600 requests per window) and per source IP (default 1200 per window). The defaults are deployment-tunable (`HYPERSCALE_RATE_LIMIT_PER_KEY_MAX`, `HYPERSCALE_RATE_LIMIT_PER_IP_MAX`), so read your live budget from the headers rather than assuming the defaults.

Every counted response carries the current state:

- `x-ratelimit-limit`: the budget for the window.
- `x-ratelimit-remaining`: requests left in the window.
- `x-ratelimit-reset`: Unix seconds when the window resets.

A request over budget is refused with HTTP 429 and the `rate_limited` error envelope; the response adds a `retry-after` header (seconds) and the envelope's `details` name the exhausted subject (`credential` or `ip`), the limit, and the reset time. Back off until the reset, then resume; retrying a mutation with the same `Idempotency-Key` is always safe.

Two narrower limiters sit in front of specific surfaces: unauthenticated identity operations (sign-up, sign-in) are throttled per IP and per email address on the same 60-second window, and the anonymous discovery documents (`/llms.txt`, served OpenAPI/GraphQL artifacts) allow 120 requests per 60 seconds per IP. Both answer over-budget requests with the same 429 shape and headers.

GraphQL at `POST /v1/graphql` uses the discovery budget of 120 requests per IP per 60 seconds and does not also debit the general 1200-request IP budget. Each root Query field costs one credential unit against the deployment-tunable 600-unit default credential budget.

## Request body limits

Operation requests accept bodies up to 1 MiB; operation inputs are small JSON documents that carry references, never blobs. Anonymous discovery endpoints accept up to 256 KiB. An oversized body is refused with HTTP 413 and the `request_body_too_large` envelope, whose details carry the exact `maxBytes`.

## GraphQL documents

A GraphQL document may select at most 20 root fields. Only the operation the request runs is priced, so a document carrying several operations costs whatever the one named in `operationName` selects and nothing for the rest. Each root field is a full admission and execution, so the cap is what bounds the work one request can buy; a document over it is refused with HTTP 400 and the `invalid_request` envelope.

Fragments are admitted and expanded where they are spread, and a field selected both beside a spread and inside it counts once, the way GraphQL merges response keys. Three further refusals keep that count exact and the expansion finite: more than 1024 field selections across the whole walk, a fragment that spreads itself either directly or through other fragments, and one field selected twice with the same arguments under two different response keys. Mutations are refused outright, because this surface serves reads only.

## Pagination bounds

Every list operation takes the same query shape: `limit` (1 to 100, default 50) and `cursor`. Where the resource supports them, `search` (free text), `status`, and `createdFrom`/`createdTo` (ISO 8601 timestamps with offset) narrow the walk.

A page returns `items` plus `nextCursor` when more results exist. The cursor is an opaque token. Never parse or construct one. A malformed cursor is refused with `invalid_request`. To read everything, loop until `nextCursor` is absent:

```js
let cursor;
do {
  const page = await client.someList({ query: { limit: 100, cursor } });
  for (const item of page.data.items) handle(item);
  cursor = page.data.nextCursor;
} while (cursor);
```

A cursor resumes the exact walk that produced it. When you change filters (search, status, date range), start over without a cursor; results are ordered newest first by creation time.

## Webhook delivery

Webhook deliveries time out after 10s and retry on a fixed backoff (1m, 5m, 30m, 2h, 6h, 24h, 72h after successive failures, 8 attempts in total) before a delivery is marked exhausted. Signing, verification, and the full delivery contract are documented in the webhook section of the setup guide.

## Observability and correlation

Every operation response carries three correlation headers:

- `x-request-id`: the request's correlation ID. Send your own `X-Request-Id` and the platform echoes it back; omit it and one is generated.
- `hyperscale-operation-id`: the executed operation's ID, quotable in `operation.retrieve` and support requests.
- `hyperscale-receipt-id`: the receipt ID for operations that produce one; the receipt is the durable outcome record.

Log all three with every call: they let a single failed request be traced end to end across your logs, the activity feed, and platform support without guesswork.

## Versioning

Every generated artifact (OpenAPI document, SDK packages, kits) is stamped with contract document version 0.1.0. The REST API is URL-versioned under `/v1`.

## Timeouts and asynchronous settlement

The platform publishes no fixed per-request server timeout. Mutations return synchronously with the operation result and a receipt where one is expected; money movement that settles later is announced through webhook events. Poll the resource's status field or await its event instead of holding a request open.

## Reserved transfers and holds

A transfer hold is the reservation surface over internal transfers. `transfer_hold.create` places the amount on hold on the source account as a reserved transfer and requires `expiresAt`, so every hold carries a hard deadline. `transfer_hold.clear` settles the hold by posting the reserved transfer (a partial clear settles the supplied amount and the ledger releases the remainder); `transfer_hold.cancel` voids it before the deadline, releasing the full amount back to the source account. Callers never expire a hold themselves: once the deadline passes, the platform's system action `transfer_hold.expire` voids the still-reserved transfer and releases the funds; it is refused while the deadline is still in the future.
