<!-- Generated from /docs/render-api. Do not edit by hand. -->
> Source: https://inspekt.gg/docs/render-api · GET /api/still returns a webp of any CS2 skin at its float, pattern seed, stickers and charm, for listing cards and link previews. Parameters, caching, errors.

# Render API

GET `/api/still` returns one `image/webp` of one CS2 item, drawn by the same renderer as the live [embed](https://inspekt.gg/docs/embed). Use the URL as the `src` of an `<img>` or as an Open Graph image.

> **The first request for a picture waits while it renders.** Every request after it is served from the cache; see [Caching and the placeholder](#caching-and-the-placeholder).

## Request

Name the item with its Steam inspect link, URL-encoded, and say how to draw it:

```http
GET https://inspekt.gg/api/still?inspect=steam%3A%2F%2Frungame%2F730%2F76561202255233023%2F%2Bcsgo_econ_action_preview%20001807209A0228043004388F85D7F30340950512E67D72&view=front&bg=rarity&w=640&h=400&key=cs2pk_your_key_here
```

That link is an AK-47 | Redline at float 0.24 and pattern 661. `view=front` turns it square-on, `bg=rarity` puts it on its Classified plate, and `w` and `h` make the picture 640 × 400.

The link sets the item: the finish, by its definition index and paint index, and its float, pattern, stickers, charm, StatTrak count and name tag. The link's rarity and quality fields are ignored, and `bg=rarity` takes the tier from the catalog. Only `view`, `bg`, `light`, `w`, `h`, `wait` and `key` are read beside the link. A link works when it carries the item's data as hex: one copied for an item inside CS2, or a "gen" link like this one, including one you build yourself. A link from a Steam Community Market listing or a Steam inventory page ends in `S…A…D…` or `M…A…D…` and is refused with a `400`.

### Catalog ids

To name an item without a link, send its catalog id and the item parameters instead. `id=ak47_cu_ak47_cobra_gen&wear=0.24&seed=661` is the same Redline and returns the same cached file.

Every catalog id is a row of [`/assets/skins.json`](https://inspekt.gg/assets/skins.json), one row per finish the service renders. A row carries the `id`, the `name` as Steam shows it (`AK-47 | Redline`), the `itemIndex` and `paintKitIndex` an inspect link carries as its definition index and paint index (`7` and `282`), and the float range the finish is made in, `wearRemapMin` to `wearRemapMax`. The file is about 0.8 MB and has no CORS headers, so fetch it from your server or a build step. An id is the row's `weapon` without its `weapon_` prefix, then its `kit`, then `_gen`: `weapon_ak47` and `cu_ak47_cobra` make `ak47_cu_ak47_cobra_gen`. The [studio](https://inspekt.gg/) shows the same id as `s=` in its address bar while the item is open.

## Response

The response is the render. The first request for a picture waits while it renders, and every request after it gets the same file from the cache:

```http
HTTP/1.1 200 OK
content-type: image/webp
cache-control: private, max-age=86400
x-cs2-counted: 1
x-request-id: b545991495f2
```

With `wait=0`, the first request gets a placeholder at once instead:

```http
HTTP/1.1 200 OK
content-type: image/webp
cache-control: no-store
x-cs2-notes: this is a placeholder at the default wear and pattern, with no stickers, charm, StatTrak counter or name tag: retry for the exact render
x-cs2-counted: 1
x-request-id: 1820c7979e64
```

When we are busy, a server may get `202 Accepted` at once, uncounted, with `Retry-After` in seconds and `x-cs2-queued`, how many pictures are drawn before it. Ask again after the `Retry-After` and keep only a `200`. An `<img>` gets the placeholder instead.

```http
HTTP/1.1 202 Accepted
content-type: application/json; charset=utf-8
cache-control: no-store
retry-after: 3
x-cs2-queued: 12
x-cs2-counted: 0
x-request-id: 3f0a9c21d7b4

{"queued":true,"ahead":12,"retryAfter":3}
```

`x-cs2-notes` lists, separated by semicolons, what the picture shows differently from the request, such as a placeholder or a wear outside the finish's range. `x-cs2-counted` is `1` when the request counted toward the plan's render quota and `0` when it did not; see [Quota](#quota-and-billing). Every response carries an `x-request-id`. Quote it when you contact us about a request.

## Parameters

| Param | Values | Default |
| --- | --- | --- |
| inspect | A Steam inspect link, URL-encoded | required, unless `id` is sent, or `charm` for a [standalone charm](#standalone-charms), or `sticker` for a [standalone sticker](#standalone-stickers) |
| id | A catalog id, `/^[a-z0-9][a-z0-9_.-]*$/i`; see [Catalog ids](#catalog-ids) | required, unless `inspect` is sent, or `charm` for a [standalone charm](#standalone-charms), or `sticker` for a [standalone sticker](#standalone-stickers) |
| wear | `0` to `1`. A value outside the finish's own range is drawn at the nearest end of it and named in `x-cs2-notes` | the finish's default |
| seed | Whole number, `0` to `1000` | `0`, the pattern in Valve's inventory picture |
| st | Up to 5 stickers; see [Stickers](#stickers) | none |
| sticker | Sent with no `id`, one sticker by itself, by its Steam definition index; see [Standalone stickers](#standalone-stickers) | none |
| charm | One charm and where it hangs; see [Charms](#charms). Sent with no `id`, the charm by itself; see [Standalone charms](#standalone-charms) | none |
| stattrak | Whole number, `0` to `999999`. Sending it makes the item StatTrak, so `stattrak=0` is a StatTrak item at zero kills | a plain item |
| name | Name tag text, up to 20 characters, without control characters | none |
| view | `hero`, `front`, `back` or `top`; see [Views](#views) | `hero` |
| bg | A background name; see [Backgrounds](#backgrounds) | `steel` |
| light | `icon` or `inspect`; see [Lighting](#lighting) | set by `view` |
| w, h | Whole numbers, `160` to `1600`, or up to `2560` with a key on a plan; see [HQ stills](#hq-stills) | `1200` × `630` |
| wait | `1` waits for the render, up to 95 seconds. `0` answers at once, with a placeholder when the picture is not cached yet; see [Caching and the placeholder](#caching-and-the-placeholder) | `1` |
| key | Your publishable key, `/^[a-z0-9_-]{4,64}$/i`. A server can send its secret key in an `x-cs2-key` header instead; a publishable key there needs an `Origin` naming a registered origin. See [Keys and origins](#keys-and-origins) | required |

A value outside these bounds is refused with a `400` that names the parameter and its bounds; see [Errors](#errors).

## Stickers

`st` is up to five stickers separated by commas, each `slot:stickerId:wear`, optionally followed by `:offsetX:offsetY:rotation`. Leave a placement field empty to keep the slot's default: `3:1044:0.12:::90` only rotates the sticker in slot 3.

A slot is one of the sticker positions the weapon's model numbers, and an offset moves the sticker from that position's default. Since CS2's free placement, a slot can appear more than once and runs to `7`. An M4A4 | Howl carrying a sticker in slot 0, one in slot 3 and three in slot 6, with a Sticker Slab (see [Sticker Slab](#sticker-slab)):

```http
GET https://inspekt.gg/api/still?id=m4a1_cu_m4a1_howling_gen&wear=0.170842&seed=655&st=0:103:0.00,3:7916:0.98:-0.3514:0.08:151.5,6:5952:0.00:0.2242:-0.0444:6,6:7916:0.98:0.0027:0.0179:130.5,6:7916:0.98:-0.0266:0.075:171&charm=37:15.9565:0.4259:14.7431~103&view=front&w=640&h=400&key=cs2pk_your_key_here
```

| Field | Bounds | Default |
| --- | --- | --- |
| slot | `0` to `7`, as often as the item has it | required |
| stickerId | Steam definition index, `1` to `999999` | required |
| wear | `0` to `1`, up to 4 decimals | required |
| offsetX, offsetY | `-1` to `1`, up to 4 decimals | `0`, the slot's own position |
| rotation | `-360` to `360` degrees, up to 4 decimals | `0` |

- A sticker's centre has to stay inside the area CS2's sticker editor allows on that weapon. An offset that moves it outside is refused with a `400` that names the slot and the nearest offset inside; see [Errors](#errors).
- A sticker is drawn at its slot's size. A seventh field (a scale) is ignored. An item from an inspect link keeps the scale its link records.
- A sticker sent to a slot without a surface, such as slot 3 of the M249, is left off the picture, and one sent to a slot the model does not number, such as slot 6 on an AK-47, is drawn at one it does. `x-cs2-notes` names both.
- Overlapping stickers are drawn in slot order, and stickers sharing a slot in the order `st` lists them, the later one on top.
- A value out of bounds, or an `st` that fails to parse, refuses the whole request.

### Standalone stickers

Send `sticker` with no `id` and no `inspect`, and the picture is the sticker by itself, flat and square-on, as its inventory picture shows it. The value is the sticker's Steam definition index, the same `stickerId` that `st` takes. Paper, holo, foil, gold, glitter and lenticular stickers are each drawn in their own finish. Space Wolves on a transparent background, 512 × 384:

```http
GET https://inspekt.gg/api/still?sticker=9362&bg=transparent&w=512&h=384&key=cs2pk_your_key_here
```

Space Wolves, `sticker=9362`

It is a still of its own, cached and counted as any still. `bg`, `w`, `h`, `wait` and `key` work as they do for a gun. A parameter that describes a gun (`wear`, `seed`, `st`, `stattrak`, `name`, `charm`, a `view` other than `hero`, `light=inspect` or `bg=rarity`) is refused with a `400` that names it, and so is a number with no sticker behind it.

## Charms

`charm` hangs one charm on the gun: any of the 143 charms in CS2, from any keychain spot the game offers on that gun, at any pattern seed. The value is the charm's Steam definition index, optionally followed by `:offsetX:offsetY:offsetZ`, a point on the gun that picks the spot, then by `@seed`, and then, for a Sticker Slab, by `~stickerId`, the sticker sealed in it; see [Sticker Slab](#sticker-slab). On the M4A1-S | Fade, `charm=60` hangs Lil' Chirp from the default spot, `charm=60:-10.4688:0.345:2.4583` moves it to the stock end, and `charm=60@75000` hangs it from the default spot at seed 75000, teal where seed 0 is pink. The [overview](https://inspekt.gg/docs#charms) shows four charms beside Valve's pictures of them. Sent with no `id`, `charm` draws the charm by itself; see [Standalone charms](#standalone-charms).

| Field | Bounds | Default |
| --- | --- | --- |
| defIndex | Steam definition index, `1` to `999999`; see [Finding a charm](#finding-a-charm) | required |
| offsetX, offsetY, offsetZ | `-64` to `64`, up to 4 decimals, in Source units (inches) in the gun model's own space; see [Spots](#charm-spots) | the gun's default spot |
| seed | Whole number, `0` to `100000`: the charm's Charm Template in CS2; see [Seeds](#charm-seeds) | `0` |
| stickerId | Steam definition index, `1` to `999999`: the sticker sealed in a Sticker Slab; see [Sticker Slab](#sticker-slab) | none |

### Examples

Each of these draws a 640 × 400 picture from the front. Glitter Bomb on the Glock-18 | Neo-Noir, from the default spot:

```http
GET https://inspekt.gg/api/still?id=glock_cu_glock_noir_gen&wear=0.03&seed=100&charm=73&view=front&w=640&h=400&key=cs2pk_your_key_here
```

Lil' Chirp on the Desert Eagle | Printstream at seed 75000:

```http
GET https://inspekt.gg/api/still?id=deagle_cu_deag_printstream_gen&wear=0.03&seed=100&charm=60@75000&view=front&w=640&h=400&key=cs2pk_your_key_here
```

Glitter Bomb at the stock end of the AK-47 | Redline, at seed 40000:

```http
GET https://inspekt.gg/api/still?id=ak47_cu_ak47_cobra_gen&wear=0.24&seed=661&charm=73:-9.874:0.718:-1.735@40000&view=front&w=640&h=400&key=cs2pk_your_key_here
```

8 Ball IGL on the same AK-47, sent a point 35 inches past the muzzle, which hangs it from the spot nearest that point, at the muzzle end:

```http
GET https://inspekt.gg/api/still?id=ak47_cu_ak47_cobra_gen&wear=0.24&seed=661&charm=78:60:0:0&view=front&w=640&h=400&key=cs2pk_your_key_here
```

### Finding a charm

Each charm is a row of [`/assets/charms/index.json`](https://inspekt.gg/assets/charms/index.json):

```json
{"id":"kc_missinglink_lilhatchling","name":"Lil' Chirp","defIndex":60,"icon":"econ/charms/kc_missinglink_lilhatchling.webp","group":"Missing Link Community Charm Collection"}
```

`defIndex` is the number `charm` takes, and `name` is the charm's name as Steam shows it. `group` is its collection or tournament. `icon` is Valve's inventory picture of the charm, a 256 × 192 webp at that path under `/assets/`, so Lil' Chirp's is [`/assets/econ/charms/kc_missinglink_lilhatchling.webp`](https://inspekt.gg/assets/econ/charms/kc_missinglink_lilhatchling.webp). The file has no CORS headers, so fetch it from your server or a build step.

An inspect link names its charm by the same number, the keychain's `sticker_id`, and carries the charm's spot, seed and sealed sticker as well, so a request with `inspect` draws the charm where its owner hung it. Every gun takes a charm. A charm sent with a knife or gloves is left off the picture.

### Sticker Slab

The Sticker Slab, `defIndex` 37, holds one sticker, named by its Steam definition index after a `~`: `charm=37~103` hangs a Slab with sticker 103 sealed in it from the default spot, and `charm=37:15.9565:0.4259:14.7431@512~103` from another spot at seed 512. The sticker is drawn in the window below the Slab's label, where the game's own model places it. `charm=37` is the Slab with no sticker, as its inventory picture shows it.

- Only the Slab holds a sticker. A sticker sent with any other charm is refused with a `400`; the still the [embed](https://inspekt.gg/docs/embed) draws leaves it out and names it in `x-cs2-notes`.
- A sticker we hold no art for is left out of the picture and named in `x-cs2-notes`.
- The sealed sticker is part of the key a still is cached under, so `charm=37` and `charm=37~103` are two stills.

### Spots

A gun's keychain spots are the ones the game lets a player drag a charm to. A `charm` without offsets hangs from the default spot, where the game first hangs one.

The three offsets are a point in the gun model's own space, in Source units (inches): X runs from the stock toward the muzzle, Y across the gun and Z up. They are the numbers an inspect link carries as its keychain's `offset_x`, `offset_y` and `offset_z`. The charm hangs from the spot nearest the point you send, so any point inside the bounds lands on the gun.

- To pick a spot by eye, open the gun in the [studio](https://inspekt.gg/), add the charm, press **Move** and click a dot. The address bar's `charm=` then holds that spot's point, and a share link saves it too.
- Send all three offsets or none. `charm=60:0:0:0` is the default spot, the same request as `charm=60`.

### Seeds

The seed is the charm's Charm Template in CS2, a whole number from 0 to 100000, and the `pattern` an inspect link carries for its keychain. It changes a charm the way the game's own charm materials do, on most charms by shifting the hue. A charm sent without a seed is drawn at seed 0, so `charm=60`, `charm=60@0` and `charm=60:0:0:0` are one still. Offsets are cached as sent, so send each spot's point the same way each time.

### Errors

A `charm` outside these rules refuses the whole request with a `400` whose message names the field and its bounds. The checks read the value's form, so a number with no charm behind it, such as `charm=34`, passes them on a gun: take the `defIndex` from the index. The [embed](https://inspekt.gg/docs/embed) drops a `charm` it cannot read and names it in `x-cs2-dropped`.

### Standalone charms

Send `charm` with no `id` and no `inspect`, and the picture is the charm by itself, as its inventory picture shows it, hanging at rest from its ring. The value is `defIndex[@seed][~stickerId]`, the same definition index, seed and sealed sticker a charm takes on a gun. Lil' Chirp at seed 75000:

```http
GET https://inspekt.gg/api/still?charm=60@75000&w=640&h=400&key=cs2pk_your_key_here
```

Lil' Chirp at seed 75000, `charm=60@75000`

It is a still of its own, cached and counted as any still, and takes `bg`, `w`, `h`, `wait` and `key` as a gun does. The parameters that describe a gun, and offsets, which name a spot on one, are refused with a `400`, as for a [standalone sticker](#standalone-stickers), and so is a number with no charm behind it.

## Views

| Value | What you get |
| --- | --- |
| `hero` | The default. The item as its Steam inventory icon shows it. A gun is shown side-on at its icon's angle, and a knife lies on the diagonal with the tip to the upper left. |
| `front` | Square-on to the sticker side through a 10° lens, muzzle left. The shot for a listing row. |
| `back` | The other flank, muzzle right. |
| `top` | Straight down, muzzle left, lined up with `front`. |

Each view fits the whole item in the frame, so match `w` and `h` to the item's shape, for example a wide frame for a rifle.

## Backgrounds

| Value | What you get |
| --- | --- |
| `steel` | The default. A cool wash at the top falling to near-black. |
| `black` | `#000000`. |
| `white` | `#ffffff`, for a listing row on a light page. |
| `charcoal` | `#141619`, the catalog cards' colour. A black finish keeps its silhouette on it. |
| `ember` | Orange rising off the bottom edge into black corners. |
| `violet` | A violet halo behind the item. |
| `rarity` | The plate of the item's own rarity tier, looked up for you. Knives and gloves get Covert, and an item whose rarity we do not recognise gets `rarity-stock`. |
| `rarity-stock` … `rarity-contraband` | One plate per CS2 rarity tier, the tier's colour rising off the bottom edge out of black: `rarity-stock`, `rarity-consumer`, `rarity-industrial`, `rarity-milspec`, `rarity-restricted`, `rarity-classified`, `rarity-covert`, `rarity-contraband`. |
| `transparent` | Only the item. The webp keeps its alpha channel, for compositing over your own background. Stills only. |

- `white` and `black` are exact in every corner, so a `white` still sits on a white page without a visible edge. The gradient backgrounds pass through lossy webp compression and can be a level off, so to match a rarity plate to your page, draw it in CSS as below.
- `bg=rarity` and the plate it resolves to are the same request and the same cached file.
- To ask for a background outside this list, email [hello@inspekt.gg](mailto:hello@inspekt.gg).

### Rarity plates in CSS

A gradient plate baked into a webp shows banding. On a page that loads a stylesheet, request `bg=transparent` and draw the plate behind the item in CSS. `plateCss()` builds it from the colours, stops and shape of the render's own plates:

```javascript
var PLATE_COLOR = {
  stock: '#9aa0a6', consumer: '#b0c3d9', industrial: '#5e98d9', milspec: '#4b69ff',
  restricted: '#8847ff', classified: '#d32ce6', covert: '#eb4b4b', contraband: '#e4ae39',
};
// [position along the ramp, share of the tier colour left there]
var PLATE_STOPS = [[0, 0.8], [0.2, 0.4], [0.42, 0.124], [0.68, 0.036], [1, 0]];
// An ellipse 95% of the frame wide and 95% tall, centred 15% below its bottom edge.
function plateCss(tier) {
  var hex = PLATE_COLOR[tier];
  if (!hex) return null;
  var n = parseInt(hex.slice(1), 16);
  var rgb = [(n >> 16) & 255, (n >> 8) & 255, n & 255];
  var stops = PLATE_STOPS.map(function (s) {
    var c = rgb.map(function (v) {
      return ('0' + Math.round(v * s[1]).toString(16)).slice(-2);
    }).join('');
    return '#' + c + ' ' + Number((s[0] * 100).toFixed(4)) + '%';
  }).join(', ');
  return 'radial-gradient(95% 95% at 50% 115%, ' + stops + ')';
}
```

Pass it the tier `bg=rarity` would resolve to, and set the result as the `background` of the element around the `<img>`. For the Redline, `plateCss('classified')` gives:

```html
<div style="background: radial-gradient(95% 95% at 50% 115%, #a923b8 0%, #54125c 20%, #1a051d 42%, #080208 68%, #000000 100%)"><img src="https://inspekt.gg/api/still?id=ak47_cu_ak47_cobra_gen&wear=0.24&seed=661&view=front&bg=transparent&w=640&h=400&key=cs2pk_your_key_here" width="640" height="400" referrerpolicy="strict-origin" alt="AK-47 | Redline, Field-Tested"></div>
```

Keep `bg=rarity` for an Open Graph image, and anywhere else a single file is fetched without a stylesheet.

## Lighting

| Value | What you get |
| --- | --- |
| `icon` | The clean studio look of the inventory. The default for `hero`. |
| `inspect` | The look of the in-game inspect screen. Every view is lit from the viewer's side, and foil and gold stickers reflect it as they do in the game. The default for `front`, `back` and `top`. |

- Send `light=icon` to draw a `front`, `back` or `top` view under the icon light.
- Any other value is refused with a `400`: "light must be one of icon, inspect".
- An `icon` still and an `inspect` still of the same item are two files, each cached and counted as a still.
- A standalone charm takes `icon` only.

## Keys and origins

A key is required. A request without one is refused with `401` and `key_required`, from another site's page and from a server alike. Image links shared from inspekt.gg carry a `sig` parameter in place of a key. They serve link previews and open in a browser tab, and inside another site's page they are refused like any request without a key.

A request with a key is checked against the key's registered origins, using the request's `Origin` header or, when it has none, the origin in its `Referer`. An `<img>` sends only the `Referer`, and a browser puts the page's origin in it by default. A registered origin is served. Any other is refused with `403` and `origin_not_allowed`.

An origin is a scheme, a host and an optional port, with no path. `http` origins register as well as `https` ones, so a page in development can use a key: register `http://localhost:3000` or `http://127.0.0.1:8461` as you would `https://example.com`. Each one counts toward the plan's [origins per key](https://inspekt.gg/docs/limits#origins-per-key).

A keyed request missing both headers is refused with `403` and `origin_required`. Put `referrerpolicy="strict-origin"` on the `<img>`, and on an `<iframe>` you build yourself, as the snippets on this site do, so the browser sends the origin even on a page whose own `Referrer-Policy` withholds the referrer. `CS2Embed.mount()` sets it on the frames it builds. From your own server, send the key with an `Origin` header naming one of the key's registered origins.

## Secret keys and tokens

The publishable key is enough when your own pages make every request. Two optional credentials, both managed in the [dashboard](https://inspekt.gg/dashboard), cover calls from your server and short-lived keys for your pages.

| Credential | Where it goes | What checks it |
| --- | --- | --- |
| `cs2pk_…` | Your pages, in the page source. | The origin list, and for the embed the browser's own `frame-ancestors`. |
| `cs2sk_…` | Your server, and your server alone. | The credential itself. It carries no origin list, so keep it out of anything a browser downloads. |
| `cs2tk_…` | Your pages, in place of the publishable key. Minted by your server and valid for up to an hour. | The same origin list the publishable key is checked against. |

### The secret key

Create one under **Secret key** in the dashboard. It is shown once, and we keep only a hash of it. Creating a new one retires the old one and every token it minted. Revoking it does the same and also turns off [Require tokens](#require-tokens).

The secret key works without an `Origin` or `Referer` header, so a backend can call with it, and it is exempt from the [daily limit per visitor](https://inspekt.gg/docs/limits#per-visitor-daily-limit). It counts toward the same monthly quota and requests-per-second allowance as the publishable key, and appears in the same usage records.

### Minting a token

A token is a publishable key that expires. Mint one with the secret key and send it in place of `key=`. It works on `/embed`, `/api/still` and `/api/inventory-lookup`, checked against the same registered origins.

```http
POST https://inspekt.gg/api/token
x-cs2-key: cs2sk_your_secret_key_here
content-type: application/json

{"ttl": 3600}

200 OK
{"ok": true, "token": "cs2tk_…", "expiresIn": 3600, "expiresAt": "2026-09-22T19:04:11.000Z"}
```

`ttl` is in seconds, from 1 to 3600, and defaults to 3600. A larger value mints a 3600-second token, and 0 or less a 1-second one. Only the secret key can mint a token; a publishable key or a token answers `403` and `secret_key_required`. Minting is rate limited per account and per address, so mint one token per server process and reuse it until it nears expiry.

```javascript
// One token per process, reused until it is nearly out of time. Minting one per
// request would spend a round trip on every page view and meet the rate limit.
let token = null;
let expiresAt = 0;

async function inspektToken() {
  if (token && Date.now() < expiresAt - 60_000) return token;
  const res = await fetch('https://inspekt.gg/api/token', {
    method: 'POST',
    headers: {
      'x-cs2-key': process.env.INSPEKT_SECRET_KEY,  // server-side only
      'content-type': 'application/json',
    },
    body: JSON.stringify({ ttl: 3600 }),
  });
  if (!res.ok) throw new Error(`inspekt token: ${res.status} ${await res.text()}`);
  const body = await res.json();
  token = body.token;
  expiresAt = Date.parse(body.expiresAt);
  return token;
}

// In the page handler, the token goes where the publishable key used to:
app.get('/listing/:id', async (req, res) => {
  const key = await inspektToken();
  const item = await lookup(req.params.id);
  res.send(renderPage({
    still: `https://inspekt.gg/api/still?inspect=${encodeURIComponent(item.inspectLink)}`
      + `&view=front&w=640&h=400&key=${key}`,
  }));
});
```

The 60-second margin leaves the browser time to use a token before it expires. A request with an expired token answers `401` and `token_expired`. A token whose signature fails, such as one minted by a secret key you have since rotated, answers `401` and `token_invalid`. In both cases, mint another.

### Require tokens

**Require tokens** is a dashboard setting, off by default. While it is on, your publishable key answers `403` and `token_required` on all three surfaces, and tokens and the secret key keep working. A token copied from your page source then works for an hour at most, and only from a registered origin.

**Turn it on in this order.** Create the secret key, send tokens from your pages, check that your traffic is served, and turn the setting on last. Turning it on while your pages still send the publishable key refuses your own visitors.

## Caching and the placeholder

A still is cached under its item and picture parameters (`id`, `wear`, `seed`, `st`, `charm`, `stattrak`, `name`, `view`, `bg`, `light`, `w`, `h`) and the renderer's version. The same values return the same file, whichever form named the item, and `view=hero` is the same request as leaving `view` off.

- **Hit.** The webp, with `cache-control: private, max-age=86400`: the viewer's browser keeps it for 24 hours, and a CDN or proxy between us and the viewer does not keep it. When the renderer changes, the URL returns the new picture.
- **Miss.** The response waits for the render, up to 95 seconds, and returns it. When we are busy it may answer at once instead; see [The render queue](#render-queue).
- **Miss with `wait=0`.** The placeholder at once, and the render is queued. The same URL returns the render once it is drawn.

A placeholder is a webp of your `w` × `h`, so an `<img>` keeps its shape when the render replaces it. It shows the item at its default wear and pattern, without stickers, charm, StatTrak counter or name tag, or a plain frame of that size when we are busy. It answers `cache-control: no-store`, and `x-cs2-notes` names it a placeholder. A request for an image gets one, uncounted, in place of a render that failed or ran out of time, so the page shows the item and its next load asks for the render again.

### During an outage

While our rendering is down, every miss gets the placeholder at once, whatever `wait` says and whoever asks, an `<img>` or a server. It is a `200` with `cache-control: no-store`, `x-cs2-notes` naming it a placeholder and `x-cs2-counted: 0`, and it is free. Show it, and ask again later: the request that gets the still is the one counted.

### On a page

- **Leave `wait` off.** The first visitor to ask for a picture waits for its render and sees the exact item.
- **Pre-warm**, so that no visitor waits. Request each still once from your server before a page shows it, for example when the listing is created, and discard the body. Send the key as `key=` or in an `x-cs2-key` header, with an `Origin` header naming one of the key's registered origins; see [Keys and origins](#keys-and-origins). The cache is shared across keys, so the `<img>` with the same parameters gets the file the pre-warm rendered. The pre-warm counts as one render.
- **Send `wait=0`** when the page must paint at once and you cannot pre-warm, such as a grid of items nobody has asked for yet. The first visitor sees placeholders, and the next load gets the renders.

You may store an image the API returns and show your copy for up to 24 hours from when we sent it. After that, request it again: [Terms 3.5](https://inspekt.gg/legal/terms#licence).

## The render queue

New pictures are drawn in turn, and requests for the same picture share one render. When we are busy, higher plans are served first, and a request whose picture is far off is answered at once, uncounted, as [Response](#response) shows. If we cannot take more, a server gets `503` with `code: "queue_full"` and a `Retry-After`. A wait that runs out is a `504`. Ask again after the `Retry-After`.

## Errors

An error body is JSON, `{"error":"…","requestId":"…"}`. An access refusal and a full render queue add a `code`, and a `404` adds the `defindex` and `paintindex` the link decoded to. An access refusal opened in a browser tab is shown as a page instead.

An access refusal to a request for an image is a webp of the request's `w` × `h` with the status and the `code` drawn on it, so the page shows a card where the image would be. A request is for an image when its `Sec-Fetch-Dest` header is `image`, or, without that header, when its `Accept` header ranks an image type above `text/html`, `application/json` and `*/*`. A request for an image gets the placeholder in place of a `502`, a `504` or a `queue_full`; see [Caching and the placeholder](#caching-and-the-placeholder).

| Status | Request | Error message |
| --- | --- | --- |
| 400 | `wear=1.5`, or any value outside the bounds above | "wear must be 0..1": the parameter and its bounds |
| 400 | `id=ak47_redline`, an id the catalog lacks | "no such skin: ak47_redline" |
| 400 | `id=ak47_cu_ak47_cobra_gen&st=0:1044:0.1:0.5`, a sticker offset past the weapon's sticker area | "st slot 0 offset puts the sticker's centre at 0.6540,-0.4380, outside the area CS2 lets a sticker go on this weapon; the nearest offset inside is 0.3238,0.0308" |
| 400 | an `inspect` link ending in `S…A…D…` or `M…A…D…`, or a value that fails to decode | "inspect link: this is a market/inventory inspect link (S…A…D…): …" |
| 400 | an `inspect` link to an item without a finish, such as a vanilla knife | "inspect link names no paint kit" |
| 404 | an `inspect` link to a finish the catalog lacks | "item 7/1234 is not in the catalog" |
| 503 | a miss we cannot take right now; see [The render queue](#render-queue) | "this caller has too many stills queued, …" or similar, with `code: "queue_full"` and `Retry-After` in seconds |
| 502 | a miss whose render fails | "still render failed: …" |
| 504 | a miss whose wait runs out, after up to 95 seconds | "still render timed out. Retry, it may be cached by then", with a `Retry-After` in seconds when its render is still queued |
| 503 | a placeholder for an item with no picture to draw it from, such as the Budapest 2025 Highlight charm | "still not rendered yet. Retry shortly", with `Retry-After: 10` |

An access refusal answers `401`, `402`, `403` or `429`, and its `code` names the cause. `key=x` gets a `401` with "That is not a valid key." and `key_unknown`. A `429` carries `Retry-After`. See [Error codes](https://inspekt.gg/docs/errors).

## Quota

A request counts once toward the plan's monthly render quota when it gets a cached still, the render it waited for, or, with `wait=0`, a placeholder whose render it queued or joined. Every other placeholder and every error is uncounted. A token changes the URL every hour, so a still fetched with one counts once per viewer, per picture, per 24 hours. `X-CS2-Counted` says `1` or `0` on every response.

Past the quota, `/api/still` answers `429` with `code: "quota_exceeded"` until the billing period renews; see [Limits & SLA](https://inspekt.gg/docs/limits#monthly-quotas).

## HQ stills

A request with a key (a publishable key, a secret key or a token) on a plan or a free trial may ask for `w` and `h` up to `2560`. A request without a key stops at `1600`. Every answer to a key that may ask for more says how much in `x-cs2-still-max`.

A still within 1600 × 1600 counts one render. A larger one counts one render per 1600 × 1600 of pixels, rounded up, cached or not, and says how many in `x-cs2-units`: 2560 × 1440 counts 2, and 2560 × 2560 counts 3. A still that counts more renders than the key has left is refused with `429` and `quota_exceeded`, and the message says how many are left. A key may ask for 30 stills larger than 1600 px a minute; past that it gets `429` and `rate_limited` with a `Retry-After`, and its smaller stills are served as before.
