SkinsharkError for any non-2xx response.
This page covers what to do with that error.
Using @skinshark/sdk
If you’re using the SDK, you get the typed error class plus narrowing
guards out of the box:
Retry-After honoring. POST/PATCH retries are gated on the presence of
idempotencyKey to prevent double-charges. The decision tree below
applies whether you’re using the SDK or raw fetch — the SDK handles the
network/timeout/5xx layer for you, and you handle the business-rule
errors at the call site.
The retry decision tree
Reconcile vs replay
Replay = send the same request again with the sameIdempotency-Key.
The server deduplicates and returns the original result. Only POST /merchant/users/{id}/fund supports this today.
Reconcile = query the API to discover whether the previous attempt
succeeded, regardless of whether you ever saw the response.
When in doubt, reconcile. Replay is only safe on idempotent endpoints.
Bounded retry helper
A reusable retry-with-backoff for safe operations:Common error keys, by category
Auth (don’t retry — fix the credential)
| Key | Cause | Fix |
|---|---|---|
MISSING_API_KEY | No api-key header. | Add it. |
INVALID_API_KEY | Key not found / wrong env. | Check value and env. |
API_KEY_REVOKED | Key was revoked in dashboard. | Rotate. |
API_KEY_IP_DENIED | Source IP not on allowlist. | Update allowlist or pin to NAT IP. |
FORBIDDEN | Calling a JWT-only route, or On-Behalf-Of on /merchant/*. | Drop the header / use the right route. |
USER_NOT_FOUND | On-Behalf-Of ID not owned by your merchant. | Check the value. |
ACCOUNT_SUSPENDED | Merchant or sub-user suspended. | Reactivate before retrying. |
Money (don’t retry — surface to user)
| Key | Cause | Fix |
|---|---|---|
INSUFFICIENT_BALANCE | Wallet balance < required. | Top up or reject the operation. |
TRANSFER_CURRENCY_MISMATCH | Merchant ≠ sub-user wallet currency. | Use deposit flow. |
INVALID_AMOUNT | Amount ≤ 0 or wrong format. | Validate input. |
MERCHANT_MUST_BE_USD | Tried to set merchant currency to non-USD. | Merchants are USD-only. |
Trade (mostly user-facing)
| Key | Cause |
|---|---|
LISTING_NOT_FOUND | Listing already sold or removed. |
PRICE_MISMATCH | Listing price moved above maxPrice. Re-quote. |
PRICE_BELOW_MINIMUM | Quick-buy maxPrice below current minimum. |
TRADEURL_REQUIRED | Sub-user has no primary Steam trade URL set. |
TRADEURL_INVALID_TOKEN | Trade URL invalid, expired, or inventory private. |
TRADEURL_ESCROW | Counterparty’s account has trade hold enabled. |
TRADE_NOT_CANCELLABLE | Trade not in a cancellable state. |
TRADE_CANCEL_TOO_SOON | Trade must be ≥ 30 minutes old to cancel. |
Idempotency
| Key | Cause |
|---|---|
IDEMPOTENCY_KEY_REQUIRED | Header missing on /fund. |
IDEMPOTENCY_CONFLICT | Same key, different body — server returns the original transaction. |
What to log
Every error has arequestId. Log it on every API call (success or fail)
so support can trace through our side. A useful client-side log line:
requestIds — they
let us pull the full request/response and ledger trace from our side
without back-and-forth.