Render API
GET /api/still returns a single
rendered image/webp of a skin, produced by the same compositor as the live embed. Use
it where a static image is the right tool: a product-card thumbnail, an Open Graph or social
unfurl image, anywhere the live 3D embed doesn't fit.
Read this before you integrate it. A cache miss does not render synchronously by default. It returns a placeholder image immediately and starts the real render in the background. See Caching and the placeholder below; it is the single most common source of "why is my image wrong" reports for this endpoint.
Request
Two forms of the same request: use whichever input you already have.
GET /api/still?inspect=steam%3A%2F%2Frungame%2F730%2F76561202255233023%2F%2Bcsgo_econ_action_preview%2000180720...&w=1200&h=630&key=cs2pk_your_key_here
GET /api/still?id=ak47_cu_ak47_cobra_gen&wear=0.1662&seed=55&w=1200&h=630&key=cs2pk_your_key_here
With inspect, the link is resolved server-side to the same normalized config the
id form uses, so both hash and cache identically. If the link can't be resolved, you get
a 400, 404 or 503 (see the response table below) with an
error, and, when the item decoded but isn't in the catalog yet, the raw
defindex / paintindex it decoded to.
Parameters
| Param | Type / bounds | Default | On failure |
|---|---|---|---|
| id | /^[a-z0-9][a-z0-9_.-]*$/i, must exist in the catalog |
required (or inspect) |
400 { error: "bad or missing id" } or 400 { error: "no such skin: <id>" } |
| inspect | Steam inspect link | required (or id) |
400, 404 or 503, message from the resolver |
| wear | number, 0-1 | kit default | 400 { error: "wear must be 0..1" } |
| seed | integer, 0-100000 | kit default | 400 { error: "seed must be an integer 0..100000" } |
| st | slot:stickerId:wear[,…], up to 5 | none | 400 { error: "st must be slot:stickerId:wear[,…] (≤5 stickers)" } |
| name | ≤20 printable characters, no control characters | none | 400 { error: "name must be at most 20 printable characters" } |
| w, h | integers, 160-1600 | 1200 × 630 |
400 { error: "w/h must be integers 160..1600" } |
| wait | 1 | unset (non-blocking) | see below |
| key | /^[a-z0-9_-]{4,64}$/i | none (anonymous) | see Error codes |
Caching and the placeholder
The render identity is a hash of the normalized config (id, wear,
seed, st, name, w, h). Two
requests with the same effective params always hit the same cached file.
- Cache hit. The webp is returned immediately, with
cache-control: public, max-age=31536000, immutablein production (the URL's identity is content-hashed, so this is safe to cache forever). - Cache miss, no
wait(the default). The request returns that skin's catalog thumbnail as a stand-in, withcache-control: no-store, and queues the real render in the background. The real render typically takes 6-19 seconds cold, including the time to start a headless browser. The placeholder is never itself cached. Request the same URL again once the render has had time to finish, and you'll get the real image. - Cache miss,
wait=1. Blocks (up to 45 seconds) until the real render finishes, then returns it directly. No placeholder is ever served on this path.
Why the default doesn't wait: this endpoint doubles as the image behind Open Graph and social-card unfurls, and most crawlers give up in a couple of seconds and cache whatever they got, for hours. Blocking by default would mean a first share of any not-yet-rendered skin gets permanently stuck with a blank card. Returning the thumbnail instead means the worst case is "looks like the flat catalog icon for a few seconds," not "broken forever."
What this means for you: if you're generating an image for a user to see live
on page load (a product card, say), either poll the same URL a couple of times a few seconds
apart until the response stops looking like the flat catalog thumbnail, or pre-warm the config
ahead of time (fetch it once, off the critical path, before a customer ever requests the page)
so it's already cached by the time it matters. Reach for wait=1 only from a
backend job or build step, where a several-second blocking call is fine and you control the
timeout. Don't reach for it from a page load.
Response
| Status | Body | Meaning |
|---|---|---|
| 200 | image/webp |
A real render (cached or freshly finished) or the placeholder thumbnail. |
| 400 | { error } |
A parameter failed validation: see the table above. |
| 503 | { error } |
With wait=1, when the request can't be queued at all: the render worker is
off, its queue is full (8 distinct configs in flight), or no browser is available on the
host. Carries Retry-After: 15. |
| 503 | { error } |
Without wait, on the rare miss where even the catalog thumbnail used as the
placeholder is missing from disk. Carries Retry-After: 10. |
| 504 | { error } |
Only with wait=1: the render didn't finish inside 45 seconds. It may still
finish and land in the cache. Retry the same request without wait and you may
get it immediately. |
Access-control refusals (bad key, wrong origin, over quota) use the same JSON error shape at
401/403/429. See Error codes.
Quota and billing
Every served render counts toward your monthly quota: a cache hit, a placeholder, and a fresh render all count the same. Overage is a separate question. Past quota, only a render that actually costs compute is recorded as overage. A cache hit costs nothing, and so does a request that joins a render already in flight. The request that starts a new render is recorded like a real render, even though what it gets back right away is the placeholder. Nothing is charged automatically today, so overage shows up as an estimate rather than a bill. See Limits & SLA for the exact rule.