<!-- Generated from /docs/errors. Do not edit by hand. -->
> Source: https://inspekt.gg/docs/errors · Every access-control error code across the embed and APIs, how each surface reports it, and the item-level fallback codes inside the embed.

# Error codes

Check the `code` field first. It puts the failure in one of two families, and each family has its own fix.

- **[Access codes](#access-codes)** mean the key, origin or quota check refused the request. The embed and both JSON APIs share them. Fix the key, the origin or the plan.
- **[Fallback codes](#fallback-codes)** come from the embed alone. Access was granted and the frame loaded, and then the item could not be rendered in the visitor's browser. A new key or origin will not fix one.

## Request IDs

Every response carries an `x-request-id` header, and every error repeats it in the body as `requestId`: a 12-character hex string, unique to that request.

a refusal, with its id:

```http
HTTP/1.1 401 Unauthorized
x-request-id: 4f9c1ad3e0b7

{"error":"That is not a valid key.","code":"key_unknown","requestId":"4f9c1ad3e0b7"}
```

Log it, and include it when you contact us, especially about a 5xx. With the id, we can find the exact request in our logs.

## Access codes

| Code | HTTP status | Meaning |
| --- | --- | --- |
| key_required | 401 | The request has no key. Add `key=<your key>`, or send your secret key from a backend in an `x-cs2-key` header. A publishable key sent in that header with no `Origin` is refused with `origin_required`. |
| key_unknown | 401 | The key is unrecognized: wrong format, a typo, or a key that does not exist. |
| key_disabled | 403 | The key has been disabled. If that is unexpected, email [hello@inspekt.gg](mailto:hello@inspekt.gg). |
| tenant_suspended | 403 | The account is suspended, and every request on it is refused. Email [hello@inspekt.gg](mailto:hello@inspekt.gg). |
| plan_required | 402 | The account has no plan: it is new, its free trial has ended, or its subscription has ended. Every request is refused until the account starts a free trial or subscribes in the [dashboard](https://inspekt.gg/dashboard). See [Terms 5.7 and 5.8](https://inspekt.gg/legal/terms#plans). |
| not_in_plan | 402 | The plan leaves this request out, for example a [Lens](https://inspekt.gg/docs/extension) key on the Inventory API. Move to Hobby or Pro in the [dashboard](https://inspekt.gg/dashboard). |
| origin_not_allowed | 403 | The requesting page's origin is not registered for this key. Add it in the [dashboard](https://inspekt.gg/dashboard). [Keys and origins](https://inspekt.gg/docs/render-api#keys-and-origins) explains how the origin is read. |
| origin_required | 403 | The request carried a key without an `Origin` or `Referer` header. Put `referrerpolicy="strict-origin"` on the `<img>`, and on the `<iframe>` if you build it yourself (`CS2Embed.mount()` sets it), so the browser sends the origin even when the page's `Referrer-Policy` withholds the referrer. From a server, send an `Origin` header naming a registered origin. See [Keys and origins](https://inspekt.gg/docs/render-api#keys-and-origins). |
| token_required | 403 | The account has **Require tokens** on, and the request carried the publishable key. Send a token minted by your server with the secret key. See [Require tokens](https://inspekt.gg/docs/render-api#require-tokens). |
| token_expired | 401 | The token has expired. Mint another with your secret key. The [Node example](https://inspekt.gg/docs/render-api#minting-a-token) replaces its token a minute before it expires. |
| token_invalid | 401 | The token is one we did not issue, or the secret key that minted it has been rotated or revoked, or the account's publishable key has been rotated since. Mint another. |
| secret_key_required | 403 | `POST /api/token` was called with a publishable key or a token. Only the account's secret key can mint a token. See [Minting a token](https://inspekt.gg/docs/render-api#minting-a-token). |
| quota_exceeded | 429 | The plan's monthly quota for this kind of request is spent. Requests of that kind are refused until the quota resets at the end of the billing period, the time given in the `Retry-After` header and the error message. Other kinds keep working, and moving up a plan takes effect on the next request. See [Limits & SLA](https://inspekt.gg/docs/limits#monthly-quotas). |
| ip_daily_limit | 429 | One visitor address has reached the account's daily limit for this kind of request on this key. `Retry-After` points at the next 00:00 UTC. Other visitors are served as usual. Set the limit, or turn it off, in the [dashboard](https://inspekt.gg/dashboard); see [Daily limit per visitor](https://inspekt.gg/docs/limits#per-visitor-daily-limit). |
| rate_limited | 429 | Requests arrived faster than the plan's requests-per-second allowance, which all three surfaces share. Retry after the number of seconds in `Retry-After`. See [Requests per second](https://inspekt.gg/docs/limits#burst-limit). |

A refused request does not count toward any quota.

### Refusals by surface

| Surface | Reported as |
| --- | --- |
| `/api/still`, `/api/inventory-lookup` | JSON: `{ error, code, requestId }` (plus `limit`/`used` for `quota_exceeded` and `ip_daily_limit`), at the status above. On `/api/still`, a request for an image, such as an `<img>`, gets a webp card of the requested `w` × `h` with the status and the code drawn on it, at the same status. See [Render API](https://inspekt.gg/docs/render-api#errors). |
| `/embed` | A refusal card and a postMessage `error` event. See below. |

> **The embed is a page, so it cannot return JSON.** A refused `/embed` does both of these at once:
>
> 1. **Draws a refusal card** in place of the viewer, with one sentence and the error code. The card can be framed on any site, so it appears wherever the misconfiguration is.
> 2. **Fires a postMessage `error` event**: `{ channel: "cs2embed", type: "error", code, message }`, addressed to the `origin` you set, or `*` with none. See [postMessage API](https://inspekt.gg/docs/postmessage).
>
> Use the card while you integrate, and log the event in production.

The visitor's browser also enforces the origin list. A served `/embed` sends the key's registered origins in a `Content-Security-Policy: frame-ancestors` header, so the browser draws the viewer only when every page around it is on that list. The refusal card goes out with `frame-ancestors *`, so it shows on whichever site used the key.

## Fallback codes (embed only)

These occur after access is granted, when the item cannot be shown in the visitor's browser. The frame keeps the still it has already painted and puts the reason in a note over it. A frame with no still shows a 2D card in place of the 3D view. Either way, it fires a postMessage `fallback` event: `{ channel: "cs2embed", type: "fallback", code, message, id }`.

| Code | Card text | Cause |
| --- | --- | --- |
| no-webgpu | "The live 3D view needs WebGPU. Open this in Chrome or Edge." | The visitor's browser has no WebGPU support. |
| no-item | "No item was specified." | Both `inspect` and `s` are missing. |
| bad-inspect | "That inspect link could not be read." | The `inspect` value cannot be decoded, for example a Steam Market or inventory link ending in `M…A…D…` or `S…A…D…`. |
| not-in-catalog | "This finish is not renderable yet." | The inspect link names a real item and finish that the catalog does not have yet. |
| unknown-id | "That item is not in the catalog." | The `s` catalog id matches no item. |
| index-unavailable | "The viewer is temporarily unavailable." | The catalog index failed to load. |
| boot-failed | "The 3D renderer could not start on this device." | WebGPU was available, and the renderer failed to initialize. |
| load-failed | "This item could not be loaded." | The item's assets failed to load after the renderer started. |

If you build embed URLs from your own data, validate the inspect links and catalog ids first. Invalid ones produce `bad-inspect` and `unknown-id`, the two fallback codes an integration produces most often.
