> ## 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.

# WebSocket events (reference)

> This is a documentation-only path — there is no HTTP endpoint at
`/ws/events`. Use it to navigate the message schemas pushed over
`/ws`.

Discriminate on `event`:

- `connected` → WsConnectedEvent
- `deposit.transfer` → WsDepositTransferEvent (gatepay on-chain detected)
- `deposit.completed` → WsGatewayDepositCompletedEvent **or** WsCryptoDepositCreditedEvent (distinguish by `data.fundingId` vs `data.depositId`)
- `deposit.failed` → WsDepositFailedEvent **or** WsCryptoDepositFailedEvent (distinguish by `data.fundingId` vs `data.depositId`)
- `deposit.refunded` → WsDepositRefundedEvent
- `deposit.pending` → WsCryptoDepositPendingEvent
- `trade.<status>` → WsTradeUpdateEvent
- `system.notice` → WsSystemNoticeEvent




## OpenAPI

````yaml /openapi.yaml get /ws/events
openapi: 3.1.0
info:
  title: SkinShark Merchant API
  description: >
    REST + WebSocket surface that you call **server-to-server** with your API
    key.

    Use it to manage sub-users, query wallets and trades, and act on behalf of
    any

    of your sub-users from a single key.


    ## Authentication


    Send your API key on every request:


    ```

    api-key: <your-key>

    ```


    Keys are hashed at rest and may be restricted to specific source IPs.
    Requests

    from a non-allowlisted IP are rejected with `API_KEY_IP_DENIED`.


    ### Acting on behalf of a sub-user


    For routes **outside** `/merchant/...`, add a header to scope the call to a

    specific sub-user:


    ```

    On-Behalf-Of: <subUserId | externalId>

    ```


    Both the UUID and the `externalId` you assigned are accepted; the server

    resolves either, scoped to your merchant. A UUID that belongs to a different

    merchant returns `USER_NOT_FOUND`.


    Without `On-Behalf-Of`, calls run **as your merchant account itself**. For

    example, `POST /market/buy` without the header buys for the merchant — its

    own spot wallet, its own Steam trade URL.


    ### Route groups


    | Prefix | What it does | `On-Behalf-Of` |

    |---|---|---|

    | `/merchant/...` | Merchant-level reads/writes (sub-users, trades
    aggregation, wallet, profile, fees). | Rejected — sub-users do not hold the
    merchant role. |

    | Everything else (`/user/...`, `/market/...`, `/auth/ws-token`) |
    User-scope operations. | Accepted — call runs as the targeted sub-user. |


    Each operation states the required context under **Auth context**.


    ## Dashboard-only operations


    A few account-management operations are not exposed to API key auth — they

    require signing in to the merchant dashboard:


    - Creating, rotating, and revoking API keys

    - Configuring webhook endpoints, secrets, and inspecting deliveries

    - Adjusting merchant fees

    - Account settings (password, email, 2FA, account deletion)

    - Audit logs and CSV exports


    Everything else in this document is available via API key.


    ## Response envelope


    Every JSON response is wrapped. Successful responses:


    ```json

    {
      "requestId": "req-...",
      "success": true,
      "data": { }
    }

    ```


    Error responses:


    ```json

    {
      "requestId": "req-...",
      "success": false,
      "error": {
        "code": 1500,
        "key": "INSUFFICIENT_BALANCE",
        "message": "Insufficient balance"
      }
    }

    ```


    Schemas below describe only the inner `data` shape — the envelope is
    implicit.


    ## Money format


    - Fields suffixed `Cents` are integer cents as JSON numbers
      (`15000` = $150.00).
    - Decimal-currency fields like `balance`, `totalPrice`, `gmv` are JSON
    numbers
      in the wallet currency (`5.23` = $5.23 in a USD wallet).
    - Amount **inputs** (e.g. `fund`, deposit `amount`) accept decimal strings
      (`"5.50"`) to avoid floating-point loss on the wire.

    ## Pagination


    Two styles are used, documented per endpoint:


    - **Page-based** — `?page=1&limit=25`. Response contains
      `{ items|users, total, page, limit, totalPages }`.
    - **Cursor-based** — `?cursor=<id>&limit=25`. Response contains
      `{ items|trades|transactions, nextCursor }`. `nextCursor` is `null` on the
      last page.

    ## Idempotency


    `POST /merchant/users/{id}/fund` requires an `Idempotency-Key` header.
    Replays

    with the same key return `idempotent: true` alongside the original

    transaction id, so retries are safe.


    ## Sub-user identifiers


    Anywhere a sub-user is referenced — the `On-Behalf-Of` header, `{id}` path

    params under `/merchant/users/...`, the `subUserId` filter on trade lists —

    the value can be **either**:


    - the sub-user's UUID, or

    - the `externalId` you assigned.


    The server resolves both, scoped to your merchant.
  version: 0.4.3
  contact:
    name: SkinShark Engineering
    email: support@skinshark.gg
  license:
    name: Proprietary
    url: https://skinshark.gg/terms
servers:
  - url: https://api.skinshark.gg
    description: Production
  - url: https://api-staging.skinshark.gg
    description: Staging
security:
  - apiKeyAuth: []
tags:
  - name: Account
    description: Merchant identity, fees, stats, wallet, ledger. Merchant context only.
  - name: Users
    description: >-
      Sub-user CRUD plus per-user wallet, ledger, trades, and funding. Merchant
      context only.
  - name: Trades
    description: Aggregate trade list across all sub-users. Merchant context only.
  - name: Profile
    description: >-
      Read the actor's profile (Steam link, Discord link, wallet snapshot).
      Merchant or sub-user context.
  - name: Trade URLs
    description: Manage Steam trade URLs for the actor. Merchant or sub-user context.
  - name: Wallet
    description: Actor's spot balance and ledger. Merchant or sub-user context.
  - name: Deposits
    description: >-
      Fund the actor's wallet via Gate Pay, on-ramp (card), or self-hosted EVM
      crypto. Merchant or sub-user context.
  - name: Partner Payout Custody
    description: >
      Separate per-merchant USD-denominated crypto custody (USDT/USDC at MVP).
      Funds deposited here

      are **not** spendable in the spot/skin-buying pipeline; they're
      withdrawable on-chain to any

      partner-supplied destination. All withdrawals require a synchronous
      approval callback to the

      merchant's registered `CallbackUrl` and a partner-supplied `externalId`
      for idempotency.

      Toggleable per merchant (`cryptoPayoutEnabled`).
  - name: Market
    description: >-
      Catalog search, listings, buy / quick-buy, and the actor's own trades.
      Merchant or sub-user context.
  - name: WebSocket
    description: >-
      Real-time trade and deposit events for a single sub-user
      (consumer-facing). Sub-user context only.
paths:
  /ws/events:
    get:
      tags:
        - WebSocket
      summary: WebSocket events (reference)
      description: >
        This is a documentation-only path — there is no HTTP endpoint at

        `/ws/events`. Use it to navigate the message schemas pushed over

        `/ws`.


        Discriminate on `event`:


        - `connected` → WsConnectedEvent

        - `deposit.transfer` → WsDepositTransferEvent (gatepay on-chain
        detected)

        - `deposit.completed` → WsGatewayDepositCompletedEvent **or**
        WsCryptoDepositCreditedEvent (distinguish by `data.fundingId` vs
        `data.depositId`)

        - `deposit.failed` → WsDepositFailedEvent **or**
        WsCryptoDepositFailedEvent (distinguish by `data.fundingId` vs
        `data.depositId`)

        - `deposit.refunded` → WsDepositRefundedEvent

        - `deposit.pending` → WsCryptoDepositPendingEvent

        - `trade.<status>` → WsTradeUpdateEvent

        - `system.notice` → WsSystemNoticeEvent
      operationId: wsEventsReference
      responses:
        '200':
          description: (reference only)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WsEvent'
        4XX:
          description: (reference only — this path has no HTTP endpoint)
components:
  schemas:
    WsEvent:
      description: Discriminated union of every server-pushed frame on `/ws`.
      oneOf:
        - $ref: '#/components/schemas/WsConnectedEvent'
        - $ref: '#/components/schemas/WsDepositTransferEvent'
        - $ref: '#/components/schemas/WsGatewayDepositCompletedEvent'
        - $ref: '#/components/schemas/WsDepositFailedEvent'
        - $ref: '#/components/schemas/WsDepositRefundedEvent'
        - $ref: '#/components/schemas/WsCryptoDepositPendingEvent'
        - $ref: '#/components/schemas/WsCryptoDepositCreditedEvent'
        - $ref: '#/components/schemas/WsCryptoDepositFailedEvent'
        - $ref: '#/components/schemas/WsTradeUpdateEvent'
        - $ref: '#/components/schemas/WsSystemNoticeEvent'
    WsConnectedEvent:
      type: object
      description: First frame the server sends after a successful upgrade.
      required:
        - event
        - data
        - ts
      properties:
        event:
          type: string
          enum:
            - connected
        data:
          type: object
          required:
            - userId
          properties:
            userId:
              type: string
              format: uuid
        ts:
          type: integer
          description: Unix epoch milliseconds.
    WsDepositTransferEvent:
      type: object
      description: |
        Gateway transfer detected on-chain (gatepay). `status: pending`.
        Replaced by `deposit.completed` once the gateway settles the credit.
      required:
        - event
        - data
        - ts
      properties:
        event:
          type: string
          enum:
            - deposit.transfer
        data:
          type: object
          required:
            - fundingId
            - status
            - transfer
          properties:
            fundingId:
              type: string
              format: uuid
            status:
              type: string
              enum:
                - pending
            transfer:
              type: object
              required:
                - txHash
                - amount
                - chain
              properties:
                txHash:
                  type: string
                amount:
                  type: number
                chain:
                  type: string
        ts:
          type: integer
    WsGatewayDepositCompletedEvent:
      type: object
      description: Gatepay or on-ramp deposit credited.
      required:
        - event
        - data
        - ts
      properties:
        event:
          type: string
          enum:
            - deposit.completed
        data:
          type: object
          required:
            - fundingId
            - status
            - payAmountToken
            - receiveAmountUsd
            - completedAt
          properties:
            fundingId:
              type: string
              format: uuid
            status:
              type: string
              enum:
                - completed
            payAmountToken:
              type: number
            receiveAmountUsd:
              type: number
            completedAt:
              type: string
              format: date-time
        ts:
          type: integer
    WsDepositFailedEvent:
      type: object
      required:
        - event
        - data
        - ts
      properties:
        event:
          type: string
          enum:
            - deposit.failed
        data:
          type: object
          required:
            - fundingId
            - status
            - reason
          properties:
            fundingId:
              type: string
              format: uuid
            status:
              type: string
              enum:
                - expired
                - failed
                - partial
            reason:
              type: string
        ts:
          type: integer
    WsDepositRefundedEvent:
      type: object
      required:
        - event
        - data
        - ts
      properties:
        event:
          type: string
          enum:
            - deposit.refunded
        data:
          type: object
          required:
            - fundingId
            - status
          properties:
            fundingId:
              type: string
              format: uuid
            status:
              type: string
              enum:
                - refunded
        ts:
          type: integer
    WsCryptoDepositPendingEvent:
      type: object
      description: Self-hosted crypto deposit detected; awaiting confirmations.
      required:
        - event
        - data
        - ts
      properties:
        event:
          type: string
          enum:
            - deposit.pending
        data:
          type: object
          required:
            - depositId
            - chain
            - token
            - txHash
            - amountWei
          properties:
            depositId:
              type: string
              format: uuid
            chain:
              type:
                - string
                - 'null'
            token:
              type:
                - string
                - 'null'
            txHash:
              type:
                - string
                - 'null'
            amountWei:
              type: string
              description: bigint as decimal string.
        ts:
          type: integer
    WsCryptoDepositCreditedEvent:
      type: object
      description: Self-hosted crypto deposit credited.
      required:
        - event
        - data
        - ts
      properties:
        event:
          type: string
          enum:
            - deposit.completed
        data:
          type: object
          required:
            - depositId
            - chain
            - token
            - txHash
            - amountWei
            - amountUsdCents
            - confirmations
            - completedAt
          properties:
            depositId:
              type: string
              format: uuid
            chain:
              type:
                - string
                - 'null'
            token:
              type:
                - string
                - 'null'
            txHash:
              type:
                - string
                - 'null'
            amountWei:
              type: string
            amountUsdCents:
              type: string
            confirmations:
              type: integer
            completedAt:
              type: string
              format: date-time
        ts:
          type: integer
    WsCryptoDepositFailedEvent:
      type: object
      description: >-
        Self-hosted crypto deposit could not be credited (e.g. unsupported
        token).
      required:
        - event
        - data
        - ts
      properties:
        event:
          type: string
          enum:
            - deposit.failed
        data:
          type: object
          required:
            - depositId
            - chain
            - token
            - txHash
            - reason
            - confirmations
          properties:
            depositId:
              type: string
              format: uuid
            chain:
              type:
                - string
                - 'null'
            token:
              type:
                - string
                - 'null'
            txHash:
              type:
                - string
                - 'null'
            reason:
              type: string
            confirmations:
              type: integer
        ts:
          type: integer
    WsTradeUpdateEvent:
      type: object
      description: >
        Fired on every trade status change including `pending`/`initiated`.
        Webhook

        events are a narrower subset.
      required:
        - event
        - data
        - ts
      properties:
        event:
          type: string
          description: >-
            `trade.<status>` — e.g. `trade.completed`, `trade.failed`,
            `trade.canceled`, `trade.declined`.
          example: trade.completed
        data:
          $ref: '#/components/schemas/Trade'
        ts:
          type: integer
    WsSystemNoticeEvent:
      type: object
      required:
        - event
        - data
        - ts
      properties:
        event:
          type: string
          enum:
            - system.notice
        data:
          type: object
          required:
            - severity
            - message
          properties:
            severity:
              type: string
              enum:
                - info
                - warning
                - critical
            message:
              type: string
        ts:
          type: integer
    Trade:
      type: object
      required:
        - id
        - type
        - userId
        - steamId
        - tradeUrl
        - status
        - game
        - items
        - summary
        - totalPrice
        - currency
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          format: uuid
        type:
          type: string
          enum:
            - buy
            - sell
        userId:
          type: string
        steamId:
          type: string
        tradeUrl:
          type: string
        offerId:
          type: string
        externalId:
          type: string
        status:
          $ref: '#/components/schemas/TradeStatus'
        game:
          type: string
          example: '730'
        items:
          type: array
          items:
            $ref: '#/components/schemas/TradeItem'
        summary:
          type: object
          required:
            - total
            - delivered
            - completed
            - failed
          properties:
            total:
              type: number
            delivered:
              type: number
              description: >-
                Items delivered to the buyer: hold (in the trade-hold window) +
                completed.
            completed:
              type: number
              description: Items whose hold has passed and settled.
            failed:
              type: number
        totalPrice:
          type: number
        currency:
          $ref: '#/components/schemas/Currency'
        holdEndDate:
          type: string
          format: date-time
        settledAt:
          type: string
          format: date-time
        revertedBy:
          type: string
          enum:
            - supplier
            - user
        error:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    TradeStatus:
      type: string
      enum:
        - initiated
        - pending
        - active
        - hold
        - completed
        - failed
        - canceled
        - declined
        - reverted
      description: >
        Terminal outcomes — `completed` (delivered), `failed` (couldn't be
        placed/delivered; see item `error`),

        `canceled` (canceled before delivery — full refund, no penalty),
        `declined` (buyer-fault decline after the

        Steam offer was sent — refund minus a 2% penalty), `reverted` (delivered
        then recalled by the supplier).
    TradeItem:
      type: object
      required:
        - id
        - name
        - marketHashName
        - type
        - iconUrl
        - price
      properties:
        id:
          type: string
          description: >-
            Stable handle for this item, for its whole life. Pass it as `itemId`
            when cancelling.
        externalId:
          type: string
          description: >-
            Your own reference for this item, echoed back when you supplied one
            at buy time. Also accepted as `itemId` when cancelling.
        name:
          type:
            - string
            - 'null'
        marketHashName:
          type:
            - string
            - 'null'
        type:
          type:
            - string
            - 'null'
        iconUrl:
          type:
            - string
            - 'null'
        price:
          type: number
        exterior:
          type: string
        rarity:
          type: string
        color:
          type: string
        phase:
          type: string
        wear:
          type: string
        paintSeed:
          type: number
        stickers:
          type: array
          items:
            type: object
            required:
              - name
              - slot
              - iconUrl
            properties:
              name:
                type: string
              slot:
                type: number
              wear:
                type: number
              iconUrl:
                type: string
        charm:
          type: object
          required:
            - name
            - iconUrl
          properties:
            name:
              type: string
            pattern:
              type: string
            iconUrl:
              type: string
        delivery:
          type: string
        status:
          $ref: '#/components/schemas/TradeStatus'
        tradable:
          type: boolean
        error:
          type: string
          enum:
            - LISTING_UNAVAILABLE
            - NO_LISTING_AT_PRICE
            - PRICE_CHANGED
            - TRADE_URL_INVALID
            - STEAM_ACCOUNT_RESTRICTED
            - BUYER_TRADE_RESTRICTED
            - MARKET_UNAVAILABLE
            - PURCHASE_FAILED
          description: >
            Set when the item `status` is `failed`. Stable partner-facing code:

            `LISTING_UNAVAILABLE` (the listing sold/was delisted — retry another
            listing),

            `NO_LISTING_AT_PRICE` (nothing on sale at or below your max —
            re-quote higher or wait for restock),

            `PRICE_CHANGED` (price moved above your max), `TRADE_URL_INVALID`
            (buyer's Steam trade URL is invalid),

            `STEAM_ACCOUNT_RESTRICTED` (buyer's Steam account can't trade),

            `BUYER_TRADE_RESTRICTED` (the marketplace suspended this buyer, e.g.
            for repeated trade-offer no-shows,

            usually until a stated time — retrying sooner cannot succeed),
            `MARKET_UNAVAILABLE` (temporary upstream/our-side

            issue — retry later), `PURCHASE_FAILED` (unexpected).
            `canceled`/`declined` items carry no code; the status is the
            outcome.
        errorDetail:
          type: string
          description: >-
            Raw marketplace reason for debugging (e.g. "c5:ITEM_SOLD item
            already sold"). Not stable — do not branch on it.
        refund:
          type: object
          description: >
            Present only on refunded items (status
            `failed`/`canceled`/`declined`/`reverted`), carried on every

            trade snapshot and webhook. `amount + penalty == price`. On a
            buyer-fault `declined` a 2% penalty is

            withheld; otherwise `penalty` is 0 and `amount` equals `price`. Buy
            trades only — sell items never carry it.
          required:
            - amount
            - penalty
            - buyerFault
          properties:
            amount:
              type: number
              description: Returned to the buyer.
            penalty:
              type: number
              description: Withheld on buyer fault, else 0.
            buyerFault:
              type: boolean
              description: True when the item was declined as the buyer's fault.
    Currency:
      type: string
      enum:
        - USD
        - EUR
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: api-key
      description: |
        Your raw API key. Generate, rotate, and revoke keys from the merchant
        dashboard. Keys can optionally be bound to one or more allowed source
        IPs — requests from any other IP are rejected with `API_KEY_IP_DENIED`.

````