> ## Documentation Index
> Fetch the complete documentation index at: https://skinshark.gg/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> Full error table — code, key, HTTP status, and meaning. Branch on `key`, never on `message`.

Every error response carries a numeric `code` and a stable string `key`.
`message` is human-readable and may change; **always branch on `key`**.

```ts theme={null}
{
  "requestId": "01900b...",
  "success": false,
  "error": { "code": 1500, "key": "INSUFFICIENT_BALANCE", "message": "..." }
}
```

See [Error handling](/docs/guides/error-handling) for retry strategy.

## Code blocks

Codes are organized into 100-block ranges by domain, with sub-groups every
10 codes inside a domain.

| Range     | Domain                              |
| --------- | ----------------------------------- |
| 1000–1099 | General / Infrastructure            |
| 1100–1199 | Auth                                |
| 1200–1299 | 2FA                                 |
| 1300–1399 | Account access                      |
| 1400–1499 | User Settings                       |
| 1500–1599 | Wallet                              |
| 1600–1699 | Deposits (gateway: gatepay, onramp) |
| 1700–1799 | Withdrawals (reserved)              |
| 1800–1899 | Crypto Deposits / Withdrawals       |
| 1900–1999 | Merchant Management                 |
| 2000–2099 | Marketplace                         |
| 2100–2199 | Sell / own Steam bots               |

## HTTP status mapping

| Status | When                                                                                         |
| ------ | -------------------------------------------------------------------------------------------- |
| `400`  | Generic bad request (`BAD_REQUEST`).                                                         |
| `401`  | Missing or invalid auth, including expired tokens.                                           |
| `403`  | Authenticated but not permitted (suspended, IP not allowed, calling a dashboard-only route). |
| `404`  | Resource doesn't exist.                                                                      |
| `409`  | Resource conflict (idempotency replay, duplicate email, etc.).                               |
| `410`  | Gone (a one-shot 2FA code was already used).                                                 |
| `422`  | Validation or business-rule failure.                                                         |
| `429`  | Rate limited.                                                                                |
| `500`  | Internal error.                                                                              |
| `502`  | Upstream gateway error (Discord OAuth, marketplace partner, crypto RPC).                     |
| `503`  | Service unavailable (no marketplace workers healthy, FX unavailable, maintenance).           |
| `504`  | Marketplace worker did not respond in time.                                                  |

## General / Infrastructure (1001–1007)

Cross-cutting errors that any endpoint can return.

| Code | Key                 | HTTP | Meaning                                                                               |
| ---- | ------------------- | ---- | ------------------------------------------------------------------------------------- |
| 1001 | `VALIDATION_FAILED` | 422  | Request body / query failed Zod schema validation. The error includes `meta.details`. |
| 1002 | `BAD_REQUEST`       | 400  | Generic malformed request (e.g. unknown query combo).                                 |
| 1003 | `NOT_FOUND`         | 404  | Generic resource not found.                                                           |
| 1004 | `CONFLICT`          | 409  | Generic uniqueness violation.                                                         |
| 1005 | `RATE_LIMITED`      | 429  | Honor `Retry-After`. See [Rate limits](/docs/reference/rate-limits).                       |
| 1006 | `INTERNAL`          | 500  | Catch-all server error. Retry.                                                        |
| 1007 | `MAINTENANCE_MODE`  | 503  | Service down for maintenance.                                                         |

## Auth (1100–1107)

| Code | Key                     | HTTP | Meaning                                                   |
| ---- | ----------------------- | ---- | --------------------------------------------------------- |
| 1100 | `UNAUTHORIZED`          | 401  | No credentials.                                           |
| 1101 | `INVALID_CREDENTIALS`   | 401  | (Dashboard JWT) bad email/password.                       |
| 1102 | `INVALID_TOKEN`         | 401  | (Dashboard JWT) bad signature, expired, or wrong purpose. |
| 1103 | `MISSING_BEARER`        | 401  | (Dashboard) no `Authorization: Bearer` header.            |
| 1104 | `MISSING_API_KEY`       | 401  | No `api-key` header.                                      |
| 1105 | `INVALID_API_KEY`       | 401  | Key not found.                                            |
| 1106 | `API_KEY_REVOKED`       | 401  | Key was revoked in dashboard.                             |
| 1107 | `INVALID_REFRESH_TOKEN` | 401  | (Dashboard) refresh token bad/expired.                    |

## 2FA (1200–1208)

| Code | Key                     | HTTP | Meaning                                      |
| ---- | ----------------------- | ---- | -------------------------------------------- |
| 1200 | `TWOFA_REQUIRED`        | 401  | Login requires 2FA challenge completion.     |
| 1201 | `INVALID_2FA_CODE`      | 422  | Bad TOTP / email / recovery code.            |
| 1202 | `TWOFA_ALREADY_ENABLED` | 409  | Cannot start TOTP setup; already enabled.    |
| 1203 | `TWOFA_NOT_ENABLED`     | 422  | Operation requires 2FA, which isn't on.      |
| 1204 | `TWOFA_NOT_STARTED`     | 422  | TOTP setup not in progress.                  |
| 1205 | `TWOFA_USE_TOTP`        | 422  | Email channel offered but account uses TOTP. |
| 1206 | `TWOFA_USE_EMAIL`       | 422  | TOTP channel offered but account uses email. |
| 1207 | `TWOFA_CODE_USED`       | 410  | Recovery code already consumed.              |
| 1208 | `TWOFA_PROVIDE_CODE`    | 422  | Provide a TOTP or recovery code.             |

## Account access (1300–1311)

| Code | Key                     | HTTP | Meaning                                                                                                                       |
| ---- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------------- |
| 1300 | `FORBIDDEN`             | 403  | Authenticated but not permitted. Most common: API-key calling a dashboard-only route, or `On-Behalf-Of` set on `/merchant/*`. |
| 1301 | `ACCOUNT_SUSPENDED`     | 403  | Merchant or sub-user account suspended.                                                                                       |
| 1302 | `ACCOUNT_DELETED`       | 403  | Account is soft-deleted.                                                                                                      |
| 1303 | `ACCOUNT_LOCKED`        | 403  | Temporarily locked (e.g. failed login bursts).                                                                                |
| 1304 | `EMAIL_NOT_VERIFIED`    | 403  | Operation requires a verified email.                                                                                          |
| 1305 | `API_KEY_IP_DENIED`     | 403  | Source IP not on the key's allowlist.                                                                                         |
| 1306 | `USER_NOT_OWNED`        | 403  | Acting on a user that doesn't belong to your merchant.                                                                        |
| 1307 | `USER_NOT_FOUND`        | 404  | Sub-user not owned by this merchant (or doesn't exist — same response, no enumeration).                                       |
| 1308 | `INVALID_INVITE`        | 422  | Invite token bad/expired.                                                                                                     |
| 1309 | `INVITE_EMAIL_MISMATCH` | 422  | Invite is for a different email.                                                                                              |
| 1310 | `INVALID_VERIFICATION`  | 422  | Email-verification code bad/expired.                                                                                          |
| 1311 | `CAPTCHA_FAILED`        | 403  | CAPTCHA verification failed.                                                                                                  |

## User Settings (1400–1422)

<Info>Codes `1415`–`1417` were added in v0.4.x for the sell flow.</Info>

| Code | Key                       | HTTP | Meaning                                                                |
| ---- | ------------------------- | ---- | ---------------------------------------------------------------------- |
| 1400 | `WRONG_PASSWORD`          | 422  | Current password incorrect.                                            |
| 1401 | `NO_PASSWORD_SET`         | 422  | Account has no password (OAuth-only).                                  |
| 1402 | `SAME_EMAIL`              | 422  | New email matches current.                                             |
| 1403 | `RESTORE_INVALID`         | 422  | Invalid/expired restore token.                                         |
| 1404 | `RESET_INVALID`           | 422  | Invalid/expired password reset token.                                  |
| 1405 | `ACCOUNT_NOT_DELETED`     | 422  | Restore called on a non-deleted account.                               |
| 1410 | `TRADEURL_LIMIT`          | 422  | Maximum number of saved trade URLs reached.                            |
| 1411 | `TRADEURL_ALREADY_ADDED`  | 409  | Trade URL already saved.                                               |
| 1412 | `TRADEURL_NOT_FOUND`      | 404  | Trade URL ID doesn't exist.                                            |
| 1413 | `TRADEURL_INVALID_TOKEN`  | 422  | Trade URL invalid, expired, or inventory private.                      |
| 1414 | `TRADEURL_ESCROW`         | 422  | Account has trade hold (escrow) enabled.                               |
| 1415 | `TRADEURL_STEAM_MISMATCH` | 422  | Trade URL doesn't belong to the Steam account linked to this user.     |
| 1416 | `TRADEURL_NOT_ALLOWED`    | 422  | A trade URL can't be passed here — the user's saved trade URL is used. |
| 1417 | `STEAM_ACCOUNT_REQUIRED`  | 403  | Selling requires a linked Steam account.                               |
| 1420 | `DISCORD_ALREADY_LINKED`  | 409  | Account already linked to a Discord.                                   |
| 1421 | `DISCORD_NOT_LINKED`      | 422  | Unlink called on an unlinked account.                                  |
| 1422 | `DISCORD_OAUTH_FAILED`    | 502  | Discord OAuth handshake failed.                                        |

## Wallet (1500–1522)

| Code | Key                          | HTTP | Meaning                                                 |
| ---- | ---------------------------- | ---- | ------------------------------------------------------- |
| 1500 | `INSUFFICIENT_BALANCE`       | 422  | Wallet balance \< required.                             |
| 1501 | `WALLET_SUSPENDED`           | 403  | Wallet is suspended (e.g. ledger drift).                |
| 1502 | `WALLET_NOT_FOUND`           | 404  | Wallet doesn't exist for that user/type.                |
| 1503 | `INVALID_CURRENCY`           | 422  | Currency value outside the supported set.               |
| 1504 | `INVALID_AMOUNT`             | 422  | Amount ≤ 0 or wrong format.                             |
| 1510 | `IDEMPOTENCY_CONFLICT`       | 409  | Same `Idempotency-Key`, different request body.         |
| 1511 | `IDEMPOTENCY_KEY_REQUIRED`   | 422  | `Idempotency-Key` header missing on `/fund`.            |
| 1520 | `TRANSFER_CURRENCY_MISMATCH` | 422  | Source and destination wallets in different currencies. |
| 1521 | `MERCHANT_MUST_BE_USD`       | 422  | Merchant accounts must use USD.                         |
| 1522 | `FX_RATE_UNAVAILABLE`        | 503  | FX rate unavailable — retry shortly.                    |

## Deposits — gateway (1600–1608)

`gatepay` and `onramp` deposits.

| Code | Key                        | HTTP | Meaning                                     |
| ---- | -------------------------- | ---- | ------------------------------------------- |
| 1600 | `DEPOSIT_NOT_FOUND`        | 404  | Unknown deposit ID.                         |
| 1601 | `DEPOSIT_NOT_CANCELLABLE`  | 422  | Only `initiated` deposits can be cancelled. |
| 1602 | `DEPOSIT_NOT_RESUMABLE`    | 422  | Deposit reached a terminal state.           |
| 1603 | `INVALID_QUOTE`            | 422  | Quote token bad or expired. Re-quote.       |
| 1604 | `INVALID_CHAIN`            | 422  | Unsupported chain for this currency.        |
| 1605 | `INVALID_DEPOSIT_CURRENCY` | 422  | Unsupported deposit currency.               |
| 1606 | `DEPOSIT_MIN_AMOUNT`       | 422  | Below provider minimum.                     |
| 1607 | `INVALID_SIGNATURE`        | 401  | Gateway callback signature failed.          |
| 1608 | `GATEWAY_ERROR`            | 502  | Payment gateway returned an error.          |

## Crypto deposits / withdrawals (1800–1811)

Self-hosted EVM deposit/withdrawal/transfer.

| Code | Key                               | HTTP | Meaning                                          |
| ---- | --------------------------------- | ---- | ------------------------------------------------ |
| 1800 | `CRYPTO_NOT_CONFIGURED`           | 503  | Crypto gateway not configured.                   |
| 1801 | `CRYPTO_CHAIN_NOT_SUPPORTED`      | 422  | Chain not enabled.                               |
| 1802 | `CRYPTO_CHAIN_PAUSED`             | 503  | Admin paused the chain.                          |
| 1803 | `CRYPTO_TOKEN_NOT_SUPPORTED`      | 422  | Token not enabled on this chain.                 |
| 1804 | `CRYPTO_ADDRESS_NOT_FOUND`        | 404  | No deposit address generated for user yet.       |
| 1805 | `CRYPTO_MIN_AMOUNT`               | 422  | Below minimum.                                   |
| 1806 | `CRYPTO_INVALID_DESTINATION`      | 422  | Destination address invalid.                     |
| 1807 | `CRYPTO_WITHDRAWAL_NOT_FOUND`     | 404  | Withdrawal not found.                            |
| 1808 | `CRYPTO_WITHDRAWAL_INVALID_STATE` | 422  | Withdrawal not in a valid state for this action. |
| 1809 | `CRYPTO_WEBHOOK_INVALID`          | 401  | Webhook signature bad.                           |
| 1810 | `CRYPTO_SELF_TRANSFER`            | 422  | Cannot transfer to your own wallet.              |
| 1811 | `CRYPTO_RECIPIENT_WALLET_MISSING` | 422  | Recipient has no wallet for this currency.       |

## Partner crypto payout custody (1820–1827)

| Code | Key                                   | HTTP | Notes                                                                                                                                                                            |
| ---- | ------------------------------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1820 | `CRYPTO_PAYOUT_NOT_ENABLED`           | 403  | Merchant does not have `cryptoPayoutEnabled=true`. Admin-controlled toggle.                                                                                                      |
| 1821 | `CRYPTO_PAYOUT_FEE_EXCEEDS_MAX`       | 422  | Live fee at submission exceeds the merchant-supplied `maxFeeUsdCents`. No state mutated.                                                                                         |
| 1822 | `CRYPTO_PAYOUT_INSUFFICIENT_BALANCE`  | 422  | Per-(chain, token) sidecar balance is below `amountCents + fee`. Each chain × token is an independent bucket — no cross-chain liquidity.                                         |
| 1823 | `CRYPTO_PAYOUT_NO_CALLBACK_URL`       | 422  | No active `CallbackUrl` registered for the merchant. Approval callbacks need somewhere to land before any withdrawal can proceed.                                                |
| 1824 | `CRYPTO_PAYOUT_DUPLICATE_EXTERNAL_ID` | 409  | Only on a **genuine concurrent duplicate** racing the first create. A normal retry of the same `externalId` returns the original withdrawal (idempotent replay), not this error. |
| 1825 | `CRYPTO_PAYOUT_INVALID_SUBUSER`       | 422  | `forSubUser` doesn't resolve to a sub-user owned by the calling merchant. The value can be the sub-user's UUID or your `externalId`, but only within your own children.          |
| 1826 | `CRYPTO_PAYOUT_FEE_UNAVAILABLE`       | 503  | The gas/price oracle is temporarily down on a fee chain. Fail-closed — retry shortly.                                                                                            |
| 1827 | `CRYPTO_PAYOUT_NO_SIGNING_KEY`        | 422  | A webhook signing secret must be provisioned before requesting payouts, so the approval call can be signed.                                                                      |

## Merchant management (1900–1950)

| Code | Key                          | HTTP | Meaning                                                                |
| ---- | ---------------------------- | ---- | ---------------------------------------------------------------------- |
| 1900 | `INVALID_ADMIN_OP`           | 422  | Invalid admin/merchant operation.                                      |
| 1910 | `APIKEY_LIMIT_REACHED`       | 422  | Maximum active API keys reached.                                       |
| 1911 | `APIKEY_ALREADY_REVOKED`     | 422  | API key already revoked.                                               |
| 1920 | `SECRET_ALREADY_EXISTS`      | 409  | Active webhook secret already exists.                                  |
| 1921 | `SECRET_NOT_FOUND`           | 404  | Webhook secret not found.                                              |
| 1922 | `SECRET_ALREADY_REVOKED`     | 422  | Webhook secret already revoked.                                        |
| 1930 | `WEBHOOK_TEST_FAILED`        | 422  | Test ping didn't return 2xx.                                           |
| 1931 | `WEBHOOK_NOT_FOUND`          | 404  | No active callback URL configured (or revoked / disabled).             |
| 1932 | `WEBHOOK_DELIVERY_NOT_FOUND` | 404  | Delivery ID not yours / doesn't exist.                                 |
| 1933 | `WEBHOOK_URL_INVALID`        | 422  | URL fails SSRF / scheme checks.                                        |
| 1940 | `EMAIL_TAKEN`                | 409  | Sub-user creation: email already registered.                           |
| 1941 | `STEAM_ID_TAKEN`             | 409  | Sub-user creation: Steam account already registered.                   |
| 1942 | `EXTERNAL_ID_TAKEN`          | 409  | Sub-user creation: `externalId` already taken by a different sub-user. |
| 1943 | `SUB_USER_HAS_BALANCE`       | 422  | Can't delete: balance is non-zero.                                     |
| 1950 | `INVALID_FEE_BPS`            | 422  | `feeBps` out of range (0–5000).                                        |

## Marketplace (2000–2041)

| Code | Key                      | HTTP | Meaning                                                   |
| ---- | ------------------------ | ---- | --------------------------------------------------------- |
| 2000 | `MARKET_UNAVAILABLE`     | 503  | No marketplace workers healthy. Retry once after 30s.     |
| 2001 | `MARKET_TIMEOUT`         | 504  | Marketplace worker did not respond in time.               |
| 2002 | `WORKER_LOW_BALANCE`     | 503  | Marketplace worker has insufficient balance to settle.    |
| 2010 | `ORDER_NOT_FOUND`        | 404  | Trade ID or external ID doesn't exist in your scope.      |
| 2011 | `LISTING_NOT_FOUND`      | 404  | Listing sold / removed since you fetched it.              |
| 2012 | `LISTING_DETAIL_FAILED`  | 502  | Failed to fetch listing details from upstream.            |
| 2020 | `PRICE_MISMATCH`         | 409  | Listing price drifted above your `maxPrice`.              |
| 2021 | `PRICE_BELOW_MINIMUM`    | 422  | Quick-buy `maxPrice` below current minimum listing price. |
| 2022 | `TRADEURL_REQUIRED`      | 422  | Sub-user has no primary Steam trade URL set.              |
| 2030 | `PURCHASE_FAILED`        | 502  | Marketplace partner rejected the purchase.                |
| 2031 | `PURCHASE_RETRY_TIMEOUT` | 504  | Purchase retried but never settled.                       |
| 2032 | `CALLBACK_INVALID`       | 401  | Marketplace callback signature failed.                    |
| 2040 | `TRADE_NOT_CANCELLABLE`  | 409  | Trade not in a cancellable state.                         |
| 2041 | `TRADE_CANCEL_TOO_SOON`  | 422  | Order must be at least 30 minutes old to cancel.          |

## Sell / own Steam bots (2100–2109)

Returned by the sell endpoints (`GET /market/sell/prices`, `GET /market/sell/inventory`,
`POST /market/sell`). See the [Selling guide](/docs/guides/selling). Selling also reuses
`PRICE_MISMATCH` (2020), `DUPLICATE_LISTING_ID` (2023), `LISTING_NOT_FOUND` (2011), and
the trade-URL codes `1415`–`1417`.

| Code | Key                         | HTTP | Meaning                                                                    |
| ---- | --------------------------- | ---- | -------------------------------------------------------------------------- |
| 2100 | `NO_BOTS_AVAILABLE`         | 503  | No Steam bots are currently available to process the trade. Retry shortly. |
| 2101 | `USER_NOT_TRADEABLE`        | 422  | Steam account can't trade (trade ban, limited account, or restriction).    |
| 2102 | `INVENTORY_PRIVATE`         | 422  | Steam inventory is private or couldn't be read.                            |
| 2103 | `STEAM_UNAVAILABLE`         | 503  | Steam is temporarily unavailable. Retry shortly.                           |
| 2104 | `TRADE_OFFER_FAILED`        | 502  | Failed to send the Steam trade offer.                                      |
| 2105 | `ESCROW_NOT_ALLOWED`        | 422  | Steam account has a trade hold (escrow) enabled.                           |
| 2106 | `STEAM_BOTS_NOT_CONFIGURED` | 503  | Steam bots aren't configured on the server.                                |
| 2107 | `NO_OFFER_FOUND`            | 422  | No payout offer is available for one or more of these items.               |
| 2108 | `ITEMS_NOT_ACCEPTED`        | 422  | One or more items aren't in the inventory or aren't tradable.              |
| 2109 | `PRICING_UNAVAILABLE`       | 503  | Payout pricing is temporarily unavailable. Retry shortly.                  |

## Validation error shape

When `key === "VALIDATION_FAILED"`, the error includes per-field details:

```json theme={null}
{
  "requestId": "01900b...",
  "success": false,
  "error": {
    "code": 1001,
    "key": "VALIDATION_FAILED",
    "message": "Validation failed",
    "meta": {
      "details": [
        { "path": ["amount"], "message": "Invalid decimal amount" },
        { "path": ["items", 0, "maxPrice"], "message": "String must contain at most 2 decimal places" }
      ]
    }
  }
}
```
