Skip to main content
The SkinShark API authenticates every request with a single header:
api-key: sk_live_...
That’s it. There is no OAuth flow, no JWT bearer header, no signed request body. Keys are issued from the merchant dashboard, hashed at rest with BLAKE3, and revocable instantly.
The header name is api-key — lowercase, hyphenated. X-API-Key is rejected.

What an API key represents

An API key is bound to one merchant account. The merchant is the parent; their sub-users hang off that parent. A key authorises:
  • All /merchant/* operations on that merchant
  • Sub-user-context routes (/user/*, /market/*, /auth/ws-token) when combined with an On-Behalf-Of header to pick which sub-user to act as
Without On-Behalf-Of, sub-user-context routes run against the merchant itself: POST /market/buy without the header buys for the merchant’s own spot wallet and uses the merchant’s primary Steam trade URL. With the header, the same call is scoped to a sub-user. See Acting on behalf of a sub-user for the full mechanics.

What API keys cannot do

Some operations live in the dashboard only and are gated behind session-based JWT auth. API keys get a 403 FORBIDDEN on:
  • API key CRUD (creating, rotating, revoking other keys)
  • Webhook URL config, secret rotation, delivery inspection
  • Adjusting merchant fees (read-only over the API)
  • Account settings (password, email change, 2FA, account deletion)
  • Audit log queries and CSV exports
If you need any of these in an automated flow, file a support request. We’ll either expose it under API-key auth or build a dedicated provisioning endpoint.

IP allowlists

When you create or rotate a key in the dashboard, you can pin it to one or more source IPs. Requests from any other IP fail with:
{
  "code": 1305,
  "key": "API_KEY_IP_DENIED",
  "message": "Request IP not in allowlist"
}
For staging and CI, either pin the key to a NAT egress IP or leave the allowlist empty during development. Never commit a non-allowlisted key.

Storing and rotating keys

  • Treat the raw key like a password. The server only stores a hash; once you lose the raw value, you must rotate.
  • Rotate by creating a new key in the dashboard and revoking the old one. Keep both active for the deploy window so callers can roll over.
  • Set per-environment keys (production vs staging vs CI) with separate IP allowlists. Don’t reuse keys across environments.

TypeScript clients

The fastest path is the official @skinshark/sdk package, which handles the headers, envelope unwrapping, retries, and typed errors for you. Keep reading if you’d rather build your own wrapper.

With @skinshark/sdk

npm install @skinshark/sdk
import { Skinshark } from "@skinshark/sdk";

const sdk = new Skinshark({ apiKey: process.env.SKINSHARK_API_KEY! });

// Merchant context
const profile = await sdk.account.get();

// Sub-user context — bind once
const user = await sdk.as("user_42");
const userProfile = await user.profile.get();

// Or per-call
await sdk.market.buy(items, "order-1", { onBehalfOf: "user_42" });
The constructor also accepts baseUrl (e.g. https://api-staging.skinshark.gg), timeoutMs, retries, userAgent, and a debug hook. See SDK & TypeScript types for the full surface.

With raw fetch

A minimal, typed wrapper that fails fast on envelope errors:
const BASE_URL = "https://api.skinshark.gg";

export class SkinsharkError extends Error {
  constructor(
    public requestId: string,
    public code: number,
    public key: string,
    message: string,
  ) {
    super(message);
  }
}

export async function api<T>(
  path: string,
  init: RequestInit = {},
  onBehalfOf?: string,
): Promise<T> {
  const headers: HeadersInit = {
    "api-key": process.env.SKINSHARK_API_KEY!,
    "content-type": "application/json",
    ...init.headers,
  };
  if (onBehalfOf) (headers as Record<string, string>)["on-behalf-of"] = onBehalfOf;

  const res = await fetch(`${BASE_URL}${path}`, { ...init, headers });
  const body = await res.json();

  if (!body.success) {
    throw new SkinsharkError(
      body.requestId,
      body.error.code,
      body.error.key,
      body.error.message,
    );
  }
  return body.data as T;
}
Use it everywhere:
// Merchant context
const profile = await api<{ id: string }>("/merchant");

// Sub-user context (note third arg)
const userProfile = await api<{ id: string }>("/user", {}, "user_42");

Common error keys

CodeHTTPKeyMeaning
1104401MISSING_API_KEYNo api-key header sent.
1105401INVALID_API_KEYKey not found.
1106401API_KEY_REVOKEDKey was active but has been revoked.
1305403API_KEY_IP_DENIEDSource IP not on the allowlist.
1301403ACCOUNT_SUSPENDEDThe merchant account is suspended.
1300403FORBIDDENTrying to call a dashboard-only endpoint, or On-Behalf-Of set on a /merchant/* route.
1307404USER_NOT_FOUNDOn-Behalf-Of references a sub-user not owned by this merchant.
See Errors for the full table.