Skip to main content
This walkthrough takes about 5 minutes. By the end you’ll have a typed client hitting the API, a sub-user with funds, and a confirmed trade.

Prerequisites

  • A SkinShark merchant API key (sk_live_...). Generate one in the dashboard.
  • Node 20+ (or any TypeScript runtime that supports fetch natively).
  • Optional: openapi-typescript for fully typed responses.

1. Set up the client

const BASE_URL = "https://api.skinshark.gg";
const API_KEY = process.env.SKINSHARK_API_KEY!;

export async function api<T>(
  path: string,
  init: RequestInit = {},
): Promise<T> {
  const res = await fetch(`${BASE_URL}${path}`, {
    ...init,
    headers: {
      "api-key": API_KEY,
      "content-type": "application/json",
      ...init.headers,
    },
  });

  const body = (await res.json()) as
    | { success: true; requestId: string; data: T }
    | { success: false; requestId: string; error: { code: number; key: string; message: string } };

  if (!body.success) {
    throw new Error(`[${body.requestId}] ${body.error.key}: ${body.error.message}`);
  }
  return body.data;
}
For full type safety, generate types from the OpenAPI spec with openapi-typescript and parameterise api<T> against the paths object.

2. Confirm credentials

GET /merchant returns your merchant profile, current fees, and wallet snapshot. If this works, your key + IP allowlist + role are all good.
const profile = await api<{
  id: string;
  email: string;
  merchantFeeBps: number;
  wallets: { spot: { currency: string; balance: number } | null };
}>("/merchant");

console.log(profile.email, "-", profile.wallets.spot?.balance, "USD");

3. Create a sub-user

A sub-user is an isolated account under your merchant: own wallet, own Steam trade URLs, own trade history. Pass an externalId you control so you can reference this sub-user with your own ID later.
POST /merchant/users is API-key auth only — it rejects dashboard sessions. This is intentional: provisioning happens server-to-server.
const sub = await api<{
  id: string;
  externalId: string | null;
  currency: "USD" | "EUR";
}>("/merchant/users", {
  method: "POST",
  body: JSON.stringify({
    email: "alice@example.com",
    externalId: "user_42",
  }),
});

console.log("Sub-user", sub.id, "external", sub.externalId);

4. Fund the sub-user

Move balance from your merchant spot wallet to the sub-user. Supply a unique Idempotency-Key so retrying after a network blip never double-funds.
import { randomUUID } from "node:crypto";

const fund = await api<{ transactionId: string; idempotent: boolean }>(
  `/merchant/users/${encodeURIComponent("user_42")}/fund`,
  {
    method: "POST",
    headers: { "idempotency-key": randomUUID() },
    body: JSON.stringify({ amount: "25.00" }),
  },
);

console.log("Funded", fund.transactionId, "(replay:", fund.idempotent, ")");

5. Find an item to buy

Acting on behalf of the sub-user, search the catalog and grab a live listing. The On-Behalf-Of header tells the API to scope this call to that sub-user — the call runs against their wallet, their Steam trade URL.
const search = await api<{
  items: Array<{ id: string; marketHashName: string; price: number }>;
}>("/market/search?q=ak-47&limit=5", {
  headers: { "on-behalf-of": "user_42" },
});

const itemId = search.items[0]!.id;

const listings = await api<{
  items: Array<{ id: string; price: number; market: string }>;
}>(`/market/items/${itemId}/listings?limit=10`, {
  headers: { "on-behalf-of": "user_42" },
});

const listing = listings.items[0]!;
console.log("Picked listing", listing.id, "$" + listing.price);

6. Buy the listing

Place the order with a maxPrice ceiling — if the listing’s price drifted above this between fetch and buy, that item fails instead of overcharging your sub-user.
const trade = await api<{
  id: string;
  status: string;
  itemCount: number;
  totalPrice: number;
}>("/market/buy", {
  method: "POST",
  headers: { "on-behalf-of": "user_42" },
  body: JSON.stringify({
    items: [
      {
        listingId: listing.id,
        maxPrice: listing.price.toFixed(2),
      },
    ],
    externalId: "checkout_2026_001",
  }),
});

console.log("Trade", trade.id, "status", trade.status);

7. Watch it execute

The trade transitions through initiated → pending → active → hold → completed. Two ways to follow it:
const detail = await api<{ status: string; settledAt?: string }>(
  `/merchant/trades/${trade.id}`,
);

Next steps

Authentication

Key types, IP allowlists, and the rules for what API-key auth can and can’t do.

Acting on behalf of

The full sub-user model, how On-Behalf-Of resolves, and what happens when you forget it.

Idempotency

Safe retries for funding, buying, and any operation that moves money.

Full Platform integration

The recommended pattern for SaaS reselling SkinShark to your own users.