Skip to main content
There are two supported ways to call the API from TypeScript. Use the official SDK if you want a polished client; generate types if you’d rather keep your own HTTP code.
npm install @skinshark/sdk
import { Skinshark, SkinsharkError, isError, meta } from "@skinshark/sdk";

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

// Merchant context
const profile = await sdk.account.get();
const stats = await sdk.account.stats({ from: "2026-01-01T00:00:00Z" });

// Sub-user context — bind once, then call as that user
const user = await sdk.as("user_42");           // validates ownership at bind time
const trade = await user.market.buy(
  [{ listingId, maxPrice: "5.00" }],
  "order-7421",                                  // your correlation id
);

// Or one-off via opts
await sdk.market.buy(items, "order-1", { onBehalfOf: "user_42" });
The package is ESM-only, Node 22+, server-to-server. It uses got under the hood, ships zero runtime deps beyond that, and exposes:
  • sdk.account, sdk.users, sdk.trades — merchant-only
  • sdk.profile, sdk.tradeUrls, sdk.wallet, sdk.deposits, sdk.market — actor-context
  • sdk.as(ref) — async, validates the ref and returns a sub-user-bound view
  • sdk.health(), sdk.newIdempotencyKey(), sdk.request<T>(...) — utilities
  • verifyWebhook(rawBody, headers, { secret }) — signature verification (zero deps beyond node:crypto)
  • meta(response) — read requestId, status, headers, rateLimit from any successful response
import { verifyWebhook, isError } from "@skinshark/sdk";

try {
  const event = verifyWebhook(rawBody, req.headers, {
    secret: process.env.SKINSHARK_WH_SECRET!,
  });
  switch (event.type) {
    case "trade.completed": /* ... */ break;
    case "deposit.completed": /* ... */ break;
  }
} catch (e) {
  if (isError(e, "INVALID_SIGNATURE")) return res.status(401).end();
  throw e;
}

Type-only imports

For UI/server code that just needs the wire types without the runtime client, import from the /types subpath:
import type {
  Trade,
  MarketListing,
  BuyBody,
  ErrorKey,
} from "@skinshark/sdk/types";

Errors

A single SkinsharkError class is thrown for every failure. Branch on the discriminated key:
import { SkinsharkError, isError, isAuthError, isRateLimited } from "@skinshark/sdk";

try {
  await sdk.market.buy(items, "order-1", { onBehalfOf: "u-1" });
} catch (e) {
  if (!(e instanceof SkinsharkError)) throw e;

  if (isError(e, "INSUFFICIENT_BALANCE")) /* refill */;
  if (isError(e, "PRICE_MISMATCH"))       /* refresh listing */;
  if (isError(e, "TRADEURL_REQUIRED"))    /* prompt user */;
  if (isAuthError(e))                     /* rotate key */;
  if (isRateLimited(e))                   await sleep(e.retryAfterMs ?? 1000);

  console.error({ key: e.key, requestId: e.requestId });   // for support
}
See Error handling for retry strategy and the full key catalogue at reference/errors.

Option 2 — generate types from the OpenAPI spec

If you’d rather not pull in the SDK runtime — for example, you’re calling from a non-Node runtime or already have an HTTP client you’re happy with — generate a paths type from the spec and pair it with a tiny fetch wrapper.
npm install -D openapi-typescript
npx openapi-typescript \
  https://api.skinshark.gg/openapi.yaml \
  -o src/skinshark-types.ts
import type { paths } from "./skinshark-types";

type CreateSubUserBody =
  paths["/merchant/users"]["post"]["requestBody"]["content"]["application/json"];

type CreateSubUserResponse =
  paths["/merchant/users"]["post"]["responses"]["201"]["content"]["application/json"]["data"];

type ListTradesQuery = NonNullable<
  paths["/merchant/trades"]["get"]["parameters"]["query"]
>;

A typed fetch helper

import type { paths } from "./skinshark-types";

type Method = "get" | "post" | "patch" | "delete";

type SuccessData<P extends keyof paths, M extends Method> =
  paths[P][M] extends { responses: { 200: { content: { "application/json": { data: infer D } } } } }
    ? D
    : paths[P][M] extends { responses: { 201: { content: { "application/json": { data: infer D } } } } }
      ? D
      : never;

export async function call<P extends keyof paths, M extends Method>(
  method: M,
  path: P,
  init?: { body?: unknown; onBehalfOf?: string; query?: Record<string, string>; idempotencyKey?: string },
): Promise<SuccessData<P, M>> {
  const qs = init?.query
    ? "?" + new URLSearchParams(init.query).toString()
    : "";

  const headers: Record<string, string> = {
    "api-key": process.env.SKINSHARK_API_KEY!,
    "content-type": "application/json",
  };
  if (init?.onBehalfOf) headers["on-behalf-of"] = init.onBehalfOf;
  if (init?.idempotencyKey) headers["idempotency-key"] = init.idempotencyKey;

  const res = await fetch(`https://api.skinshark.gg${path}${qs}`, {
    method: method.toUpperCase(),
    headers,
    body: init?.body ? JSON.stringify(init.body) : undefined,
  });

  const env = await res.json();
  if (!env.success) {
    const e = env.error;
    throw new SkinsharkError(env.requestId, e.code, e.key, e.message);
  }
  return env.data;
}
const profile = await call("get", "/merchant");
//    ^? { id: string; email: string; merchantFeeBps: number; ... }

const sub = await call("post", "/merchant/users", {
  body: { email: "alice@example.com", externalId: "user_42" },
});
//    ^? { id: string; externalId: string | null; idempotent: boolean; ... }

const trades = await call("get", "/merchant/trades", {
  query: { status: "completed", limit: "50" },
});
//    ^? { items: Trade[]; nextCursor: string | null }
Zero runtime dependencies, types drift automatically as the spec evolves, and the wrapper is ~30 lines. Pin the spec to a git tag (or a vendored copy) for stable types between releases.

Webhook signature verification

The SDK ships a verifyWebhook(rawBody, headers, { secret }) helper that follows Standard Webhooks — see the Webhooks guide for the full shape and example handlers. If you’re not using the SDK, the verification is ~30 lines of node:crypto — copy from the same guide, or use any Standard Webhooks library from npm.