Embed parameters
The smallest working embed:
CS2Embed.mount(el, { inspect: inspectLink, key: 'cs2pk_your_key_here' });
Only the item (inspect or s) and key are required. Pass
each option to CS2Embed.mount() or as a query parameter on the /embed
URL. The server and the frame both enforce the bounds. An out-of-range or malformed value is
replaced by its default and named in the X-CS2-Dropped response header. Only a query
string over 4,096 bytes is refused, with a 400.
Required input
Pass either inspect or s.
inspect: an inspect link that carries the item as hex, either copied from an item inside CS2 or built as a "gen" link. The frame decodes the item, its float and pattern, stickers, charm and StatTrak count from it. Use this by default. A Steam Community Market or Steam inventory link ends inM…A…D…orS…A…D…, carries no item data, and shows thebad-inspectcard.s: a catalog id, withwear,seed,standnamealongside it. Each id is a row of/assets/skins.json; see Catalog ids.
If you pass both, inspect wins. The frame ignores
s, wear, seed and st and uses the link's own
float, pattern, stickers, charm and StatTrak count.
Item and condition
| Param | Type / bounds | Default | Notes |
|---|---|---|---|
| inspect | Steam inspect link, up to 1,024 characters | n/a | Decoded client-side. Carries its own wear, seed and stickers; see above. |
| s | /^[a-z0-9][a-z0-9_.-]*$/i, up to 128 characters | n/a | A catalog id. An unknown id fails to load (see fallback codes). |
| wear | number, 0-1 | kit default | The float. A value outside 0 to 1, or one that is not a number, is replaced by the kit default. A value outside the finish's own range, such as 0.05 on a finish made from 0.10 to 0.70, is drawn at the nearest end of that range. |
| seed | integer, 0-1000, the game's own range | 0 |
Pattern seed. Without one, the item shows seed 0, the pattern in Valve's inventory picture. |
| st | slot:stickerId:wear[:offsetX:offsetY:rotation],
comma-separated, up to 5, slots 0-7 | none | Stickers, with CS2's free placement. Same grammar and bounds as the
Render API. A malformed string is dropped whole,
and the item is shown without stickers. An offset that puts a sticker's centre outside
the weapon's sticker area is dropped on its own. The sticker then sits at its slot's
default position, and x-cs2-dropped names the offset, as in
st slot 2 offset. A seventh field is ignored and named as
st scale. |
| charm | defIndex[:offsetX:offsetY:offsetZ][@seed][~stickerId] | none | One charm, where it hangs, its pattern seed, and the sticker sealed in a Sticker Slab. The grammar, the bounds, the charm list,
spots and seeds are in the Render API's Charms
section, and the frame's still and its 3D view draw the same charm. A malformed or
out-of-bounds value is dropped whole: the item is shown without a charm, and
x-cs2-dropped names charm. A charm we hold no model for is
skipped. |
| stattrak | integer, 0-999999 | none | The kill counter. Any value makes the item StatTrak, so stattrak=0 is a
StatTrak item with zero kills. A StatTrak knife shows no counter, because the game etches
its count into the blade. |
| name | ≤20 printable characters, no control characters | none | A name tag, trimmed. A value over 20 characters or with a control character is dropped whole. |
Scene, view and chrome
| Param | Type / bounds | Default | Notes |
|---|---|---|---|
| scene | /^(plate|pano|bg)_[a-z0-9_]{1,58}$/i | none (see bg) |
A backdrop id, such as plate_train_video. plate_ is a video
loop of a CS2 map, pano_ a 360° panorama and bg_ a plain
background. An unknown id falls back to the default and is named in
x-cs2-dropped. With scene and bg both unset, the
item stands on its rarity plate and the frame can open on a still. A video backdrop adds a
few tens of megabytes to your page. See First paint. |
| bg | rarity | black |
white | charcoal | ember | steel |
violet | the eight rarity-* tier plates |
rarity, or the backdrop scene= named |
The background behind the item. The same list as the
Render API, except the stills-only
transparent. rarity is the item's own tier plate, resolved the
same way as for a still, so the opening still and the 3D view match. bg and
scene share one setting. A plain background brings its own lighting and
replaces the backdrop, so bg wins if you send both. |
| light | icon | inspect | set by view |
The lighting, the same two with the same defaults as the
Render API, for the opening still and the 3D
view alike. It lights a plain background or a rarity plate. A scene= map
loop or panorama keeps its own light. An unknown value falls back to the default and is
named in x-cs2-dropped. |
| view | front | back | top |
hero |
front | The starting camera framing. The same set as the
Render API, where the default is
hero. |
| autorotate | 1/true to enable | off | Off when the visitor has prefers-reduced-motion set. Stops at the
visitor's first pointer-down and stays off. |
| ui | minimal | none | minimal |
none hides the name plate. The corner wordmark shows with either value.
See Frame chrome. |
| theme | dark | light | dark |
Styles the name plate and wordmark only. |
Identity and access
| Param | Type / bounds | Default | Notes |
|---|---|---|---|
| key | /^[a-z0-9_-]{4,64}$/i | required | Your publishable key. It is visible in the iframe's src and your page
source. The origins you register in the dashboard protect
it. A short-lived token from your server works here in its place; see
Minting a token. |
| origin | an http(s) origin | the page's referrer, if any | Your page's origin, where the frame sends its postMessage events.
CS2Embed.mount() sets it. Set it yourself on a hand-built iframe; see
postMessage API. |
Frame chrome
ui=minimal, the default, shows a small plate with the item's name, and
ui=none hides it. The Inspekt wordmark in the bottom-right corner shows on every
plan, with either value. Clicking it opens the item in the full studio with the same wear, seed,
stickers, charm, StatTrak count and name tag. No plan requires other attribution.
The loader: CS2Embed.mount(el, options)
The loader takes every parameter above as an options object, plus two fields of its own:
| Option | Type | Default | Notes |
|---|---|---|---|
| title | string | "3D skin viewer" |
The iframe's accessible title attribute. |
| lazy | boolean | true |
Pass false to load eagerly (loading="eager") instead of the
browser's native lazy loading. |
var viewer = CS2Embed.mount(document.getElementById('viewer'), {
inspect: 'steam://rungame/730/76561202255233023/+csgo_econ_action_preview 001807209A0228043004388F85D7F30340950512E67D72',
scene: 'plate_train_video',
view: 'front',
autorotate: true,
ui: 'minimal',
theme: 'dark',
key: 'cs2pk_your_key_here',
title: 'AK-47 | Redline in 3D',
lazy: false,
});
mount() returns a handle with the methods setItem,
setScene, setView, setAutorotate, setQuality,
resetView, screenshot and destroy, plus
on and off for events. postMessage API
lists every command and event, and the sandbox builds these snippets
from your own settings.
cs2embed.js is a script with no npm package. In React or Vue, load it once and
mount a viewer into an element:
import { useEffect, useRef } from 'react';
// Loaded once per page, and shared by every viewer.
let loaderPromise;
function loadCs2Embed() {
if (window.CS2Embed) return Promise.resolve(window.CS2Embed);
if (!loaderPromise) {
loaderPromise = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://inspekt.gg/cs2embed.js';
script.onload = () => resolve(window.CS2Embed);
script.onerror = reject;
document.head.appendChild(script);
});
}
return loaderPromise;
}
export function SkinViewer({ inspect, scene, autorotate = true }) {
const hostRef = useRef(null);
const viewerRef = useRef(null);
useEffect(() => {
let cancelled = false;
loadCs2Embed().then((CS2Embed) => {
if (cancelled || !hostRef.current) return;
viewerRef.current = CS2Embed.mount(hostRef.current, {
inspect, key: 'cs2pk_your_key_here', scene, autorotate,
});
viewerRef.current.on('error', (e) => console.error('embed refused:', e.code, e.message));
});
return () => {
cancelled = true;
viewerRef.current?.destroy();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inspect, scene, autorotate]);
return <div ref={hostRef} style={{ width: '100%', height: 400 }} />;
}
// usage: <SkinViewer inspect={item.inspectLink} />
<template>
<div ref="host" style="width: 100%; height: 400px"></div>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
const props = defineProps({
inspect: { type: String, required: true },
scene: { type: String },
});
const host = ref(null);
let viewer = null;
let loaderPromise;
function loadCs2Embed() {
if (window.CS2Embed) return Promise.resolve(window.CS2Embed);
if (!loaderPromise) {
loaderPromise = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://inspekt.gg/cs2embed.js';
script.onload = () => resolve(window.CS2Embed);
script.onerror = reject;
document.head.appendChild(script);
});
}
return loaderPromise;
}
onMounted(async () => {
const CS2Embed = await loadCs2Embed();
viewer = CS2Embed.mount(host.value, {
inspect: props.inspect,
key: 'cs2pk_your_key_here',
scene: props.scene,
autorotate: true,
});
viewer.on('error', (e) => console.error('embed refused:', e.code, e.message));
});
onBeforeUnmount(() => viewer?.destroy());
watch(() => props.inspect, (inspect) => viewer?.setItem({ inspect }));
</script>
The loader frames /embed on the host it was loaded from and accepts messages only
from that host. Load it from https://inspekt.gg with a script tag. It then frames
https://inspekt.gg/embed and sets CS2Embed.origin to
https://inspekt.gg. A copy bundled into your scripts, or served from your own site,
frames your site's /embed instead. It builds each iframe with
referrerpolicy="strict-origin", so the frame sends your page's origin even when the
page's own Referrer-Policy withholds the referrer.
Raw iframe
To embed without the loader script, build the iframe yourself. Every parameter above is a
query parameter on GET /embed:
<iframe
src="https://inspekt.gg/embed?inspect=steam%3A%2F%2Frungame%2F730%2F76561202255233023%2F%2Bcsgo_econ_action_preview%20001807209A0228043004388F85D7F30340950512E67D72&autorotate=1&key=cs2pk_your_key_here&origin=https%3A%2F%2Fyoursite.example"
style="width:640px;height:400px;border:0"
loading="lazy"
allow="autoplay; fullscreen"
referrerpolicy="strict-origin"
title="3D skin viewer"
></iframe>
Set origin on a hand-built iframe. Without it, the frame uses your page's
document.referrer, which some browsers withhold. With neither, the frame addresses
its postMessage events to *.
First paint
The frame opens on a still of your item, the same picture GET /api/still serves for that camera, background and size, and switches to the live 3D view once that view has drawn its first frame. The still is part of the embed load and is not counted as a render.
loaded fires when the item is visible, usually as the still.
interactive fires when the 3D view has drawn and accepts setView,
screenshot and the other commands. autorotate starts once the 3D view
has drawn.
The frame opens on a loading card instead of the still when:
- You set a
scenebackdrop. Stills are rendered on the default plates only. - The frame's size needs a still outside the Render API's 160 to 1600 px bounds, such as a box smaller than 160 px or wider than 10:1.
- The still is not ready before the 3D view draws, for example during an outage.
Access control
Our server checks your page's origin against the key's registered origins, and the visitor's
browser checks it again through a frame-ancestors header, so a key copied onto
another site gets the refusal card with origin_not_allowed. See
Keys and origins.
Embed loads
An embed load counts once per viewer, per item, per 24 hours, and each served
/embed response says 1 or 0 in
X-CS2-Counted. Limits & SLA has the
full rule.