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:
Poll merchant trades
WebSocket (recommended)
Webhook
const detail = await api<{ status: string; settledAt?: string }>(
`/merchant/trades/${trade.id}`,
);
const { token } = await api<{ token: string }>("/auth/ws-token", {
method: "POST",
headers: { "on-behalf-of": "user_42" },
});
const ws = new WebSocket(`wss://api.skinshark.gg/ws?token=${token}`);
ws.addEventListener("message", (e) => {
const event = JSON.parse(e.data.toString());
if (event.event.startsWith("trade.")) console.log(event);
});
Configure a callback URL in the dashboard, then handle trade.* events
with signature verification — see Webhooks.
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.