Get a key

Embed parameters

Download .md

The smallest working embed:

javascript
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.

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

ParamType / boundsDefaultNotes
inspectSteam inspect link, up to 1,024 charactersn/a Decoded client-side. Carries its own wear, seed and stickers; see above.
s/^[a-z0-9][a-z0-9_.-]*$/i, up to 128 charactersn/a A catalog id. An unknown id fails to load (see fallback codes).
wearnumber, 0-1kit 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.
seedinteger, 0-1000, the game's own range0 Pattern seed. Without one, the item shows seed 0, the pattern in Valve's inventory picture.
stslot:stickerId:wear[:offsetX:offsetY:rotation], comma-separated, up to 5, slots 0-7none 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.
charmdefIndex[: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.
stattrakinteger, 0-999999none 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 charactersnone A name tag, trimmed. A value over 20 characters or with a control character is dropped whole.

Scene, view and chrome

ParamType / boundsDefaultNotes
scene/^(plate|pano|bg)_[a-z0-9_]{1,58}$/inone (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.
bgrarity | 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.
lighticon | inspectset 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.
viewfront | back | top | hero frontThe starting camera framing. The same set as the Render API, where the default is hero.
autorotate1/true to enableoff Off when the visitor has prefers-reduced-motion set. Stops at the visitor's first pointer-down and stays off.
uiminimal | noneminimal none hides the name plate. The corner wordmark shows with either value. See Frame chrome.
themedark | lightdark Styles the name plate and wordmark only.

Identity and access

ParamType / boundsDefaultNotes
key/^[a-z0-9_-]{4,64}$/irequired 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.
originan http(s) originthe 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:

OptionTypeDefaultNotes
titlestring"3D skin viewer" The iframe's accessible title attribute.
lazybooleantrue Pass false to load eagerly (loading="eager") instead of the browser's native lazy loading.
javascript
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:

SkinViewer.jsx
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} />
SkinViewer.vue
<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:

html
<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:

  1. You set a scene backdrop. Stills are rendered on the default plates only.
  2. 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.
  3. 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.