Developer docs

Steam trade API documentation.

Connect CS2 skin deposits, withdrawals and swaps to your marketplace. Request bodies, API responses and webhook payloads below use illustrative data.

API access

Your setup includes an API base URL, a trade API key and a separate webhook signing secret. Send requests from your backend; keep both secrets on your server.

Authorization: Bearer <TRADE_API_KEY>

All paths below are relative to your assigned base URL. Retrieve the full OpenAPI specification with the same authorization:

GET /v1/docs/trade/openapi.json

Bots and inventory

GET /v1/bots
GET /v1/bots/:botId/items?current=true&limit=100&offset=0
GET /v1/items?current=true&limit=100&offset=0

Your key only sees its permitted bots. Inventory responses contain an items array. Increase offset by limit until a page contains fewer items than the limit.

current=true excludes absent and traded-away items. Current inventory can still include reserved, untradable or protected items; it does not mean every item is available to send.

200 response — bot list

Use botId in API paths. itemCount includes protected and unavailable items; hosted and loggedIn describe the latest observed session.

{
  "bots": [
    {
      "botId": "bot-eu-1",
      "displayName": "EU Storage 1",
      "status": "active",
      "activatedAt": 1790589000000,
      "createdAt": 1790589000000,
      "updatedAt": 1790589600000,
      "itemCount": 24,
      "hosted": true,
      "ownerId": "swarm-eu-1",
      "ownerHeartbeatAt": 1790589600000,
      "loggedIn": true,
      "steamId": "76561198000000001",
      "inventoryValue": {
        "currency": "USD",
        "totalQuantity": 24,
        "calculatedAt": "2026-09-28T10:00:00.000Z",
        "buy": {
          "total": 2136,
          "pricedQuantity": 24,
          "unpricedQuantity": 0,
          "riskWarningQuantity": 24,
          "validUntil": "2026-09-28T12:00:00.000Z"
        },
        "sell": {
          "total": 2016,
          "pricedQuantity": 24,
          "unpricedQuantity": 0,
          "riskWarningQuantity": 0,
          "validUntil": "2026-09-28T12:00:00.000Z"
        }
      }
    }
  ]
}
200 response excerpt — inventory with prices and a protected skin

The pricing object is abbreviated: full market arrays, compatibility aliases and some directional fields are omitted. All values are fictional.

{
  "items": [
    {
      "asset_id": "30922345678",
      "class_id": "1234567890",
      "instance_id": "0",
      "market_hash_name": "AWP | Asiimov (Field-Tested)",
      "state": "untradable",
      "first_seen_at": 1788429700000,
      "last_seen_at": 1788429710000,
      "raw": {
        "name": "AWP | Asiimov (Field-Tested)",
        "icon_url": "<STEAM_ECONOMY_ICON_PATH>",
        "type": "Covert Sniper Rifle",
        "tags": [
          {
            "category": "Exterior",
            "name": "Field-Tested"
          }
        ]
      },
      "present": true,
      "tradable": false,
      "amount": 1,
      "protected_until": 1789034500000,
      "settlement_trade_id": "90000000001",
      "pricing": {
        "currency": "USD",
        "method": "primary-market-mean-v11",
        "marketHashName": "AWP | Asiimov (Field-Tested)",
        "buy": {
          "basis": "market_ask_reference",
          "price": 89,
          "confidence": "medium",
          "warnings": [
            "partial_market_coverage",
            "single_provider",
            "provider_price_inflated"
          ],
          "validUntil": "2026-09-28T12:00:00.000Z",
          "calculation": "offer_weighted_mean",
          "sourcePolicy": "primary",
          "risk": {
            "status": "warning",
            "flaggedMarkets": [
              "buff163"
            ],
            "uncheckedMarkets": [],
            "historyWarningMarkets": [],
            "recentWarningMarkets": [],
            "missingHistoryMarkets": [],
            "missingRecentMarkets": [],
            "score": 25,
            "scoreKind": "heuristic_index",
            "scoreVersion": "manipulation-risk-v1",
            "scoreCoverage": "complete",
            "scoredMarkets": [
              "buff163",
              "youpin"
            ],
            "scoreFactors": [
              {
                "code": "provider_inflation",
                "points": 25,
                "markets": [
                  "buff163"
                ]
              }
            ]
          }
        },
        "sell": {
          "basis": "market_bid_reference",
          "price": 84,
          "confidence": "medium",
          "warnings": [
            "partial_market_coverage",
            "single_provider"
          ],
          "validUntil": "2026-09-28T12:00:00.000Z",
          "calculation": "single_quote",
          "sourcePolicy": "primary",
          "risk": {
            "status": "no_flags",
            "flaggedMarkets": [],
            "uncheckedMarkets": [],
            "historyWarningMarkets": [],
            "recentWarningMarkets": [],
            "missingHistoryMarkets": [],
            "missingRecentMarkets": [],
            "score": 0,
            "scoreKind": "heuristic_index",
            "scoreVersion": "manipulation-risk-v1",
            "scoreCoverage": "complete",
            "scoredMarkets": [
              "buff163_buy"
            ],
            "scoreFactors": []
          }
        }
      }
    }
  ]
}

protected_until is a Unix timestamp in milliseconds; null means no recorded protection deadline. Check it alongside present, tradable and state. Metadata in raw can be missing. Fleet inventory adds bot_id to each row.

Inventory rows use asset_id and protected_until; request bodies use assetId. Steam’s partner inventory and settlement assets use assetid and string amounts. Preserve these field names and types.

Read the partner’s inventory

POST /v1/partner-items
Authorization: Bearer <TRADE_API_KEY>
Content-Type: application/json
Request body — partner inventory
{
  "partnerSteamId": "76561197960278073"
}
200 response excerpt — partner inventory with prices

This endpoint selects a ready bot. Pricing is abbreviated. For the next page, resend the partner with startAssetId set to nextAssetId. Stop when nextAssetId is absent.

{
  "botId": "bot-eu-1",
  "items": [
    {
      "appid": 730,
      "assetid": "30912345678",
      "classid": "1234567890",
      "instanceid": "0",
      "amount": "1",
      "market_hash_name": "AWP | Asiimov (Field-Tested)",
      "marketable": 1,
      "tradable": 1,
      "icon_url": "<STEAM_ECONOMY_ICON_PATH>",
      "tags": [
        {
          "category": "Exterior",
          "name": "Field-Tested"
        }
      ],
      "pricing": {
        "currency": "USD",
        "method": "primary-market-mean-v11",
        "marketHashName": "AWP | Asiimov (Field-Tested)",
        "buy": {
          "basis": "market_ask_reference",
          "price": 89,
          "confidence": "medium",
          "warnings": [
            "partial_market_coverage",
            "single_provider",
            "provider_price_inflated"
          ],
          "validUntil": "2026-09-28T12:00:00.000Z",
          "calculation": "offer_weighted_mean",
          "sourcePolicy": "primary",
          "risk": {
            "status": "warning",
            "flaggedMarkets": [
              "buff163"
            ],
            "uncheckedMarkets": [],
            "historyWarningMarkets": [],
            "recentWarningMarkets": [],
            "missingHistoryMarkets": [],
            "missingRecentMarkets": [],
            "score": 25,
            "scoreKind": "heuristic_index",
            "scoreVersion": "manipulation-risk-v1",
            "scoreCoverage": "complete",
            "scoredMarkets": [
              "buff163",
              "youpin"
            ],
            "scoreFactors": [
              {
                "code": "provider_inflation",
                "points": 25,
                "markets": [
                  "buff163"
                ]
              }
            ]
          }
        },
        "sell": {
          "basis": "market_bid_reference",
          "price": 84,
          "confidence": "medium",
          "warnings": [
            "partial_market_coverage",
            "single_provider"
          ],
          "validUntil": "2026-09-28T12:00:00.000Z",
          "calculation": "single_quote",
          "sourcePolicy": "primary",
          "risk": {
            "status": "no_flags",
            "flaggedMarkets": [],
            "uncheckedMarkets": [],
            "historyWarningMarkets": [],
            "recentWarningMarkets": [],
            "missingHistoryMarkets": [],
            "missingRecentMarkets": [],
            "score": 0,
            "scoreKind": "heuristic_index",
            "scoreVersion": "manipulation-risk-v1",
            "scoreCoverage": "complete",
            "scoredMarkets": [
              "buff163_buy"
            ],
            "scoreFactors": []
          }
        }
      }
    }
  ],
  "nextAssetId": "30912345678"
}

Inventory snapshot & WebSocket

Keep a local pool of sendable items across the bots available to your API key. Protected, reserved, missing, reversed and untradable items are excluded. Removed bots are excluded too.

Read all available items

GET /v1/items/snapshot
Authorization: Bearer <TRADE_API_KEY>

The snapshot returns every available item in one response, with no pagination. Add ?botIds=bot-eu-1,bot-eu-2 to narrow the selection. This filter cannot expand your key’s access. An empty items array is a valid empty inventory; a failed read returns an error.

200 response excerpt — available inventory with prices

Fictional item. Pricing markets, risk details and optional inspect fields are abbreviated; the API returns the same enrichment as GET /items.

{
  "type": "inventory.snapshot",
  "observedAt": 1791460800000,
  "items": [
    {
      "bot_id": "bot-eu-1",
      "asset_id": "30922345678",
      "class_id": "1234567890",
      "instance_id": "0",
      "market_hash_name": "AWP | Asiimov (Field-Tested)",
      "state": "free",
      "first_seen_at": 1791374400000,
      "last_seen_at": 1791460795000,
      "raw": {
        "appid": 730,
        "contextid": "2",
        "name": "AWP | Asiimov (Field-Tested)",
        "tags": [
          {
            "category": "Type",
            "internal_name": "CSGO_Type_SniperRifle"
          }
        ],
        "asset_properties": [
          {
            "propertyid": 2,
            "float_value": 0.27
          },
          {
            "propertyid": 1,
            "int_value": 42
          }
        ]
      },
      "present": true,
      "tradable": true,
      "amount": 1,
      "protected_until": null,
      "settlement_trade_id": null,
      "itemType": "CSGO_Type_SniperRifle",
      "receiveAllowed": true,
      "inspect": {
        "status": "partial",
        "source": "asset_properties",
        "floatValue": 0.27,
        "paintSeed": 42
      },
      "pricing": {
        "currency": "USD",
        "buy": {
          "price": 89,
          "confidence": "medium",
          "validUntil": "2026-10-08T14:00:00.000Z"
        },
        "sell": {
          "price": 84,
          "confidence": "medium",
          "validUntil": "2026-10-08T14:00:00.000Z"
        }
      }
    }
  ]
}

Key items by both bot_id and asset_id. amount is the remaining sendable quantity, after consumed reservations. Each item includes pricing.buy, pricing.sell, inspect, itemType and receiveAllowed, just like GET /v1/items. Available inventory does not reserve an item or guarantee that its bot is online.

Subscribe from your backend

WSS /v1/items/stream
Authorization: Bearer <TRADE_API_KEY>

Use your assigned API host with wss://. Send the Bearer header in the WebSocket upgrade request; the optional botIds filter works here too. Browser WebSocket cannot set this header. Keep the key on your backend: cookie-only access and tokens in URLs are not supported.

// Backend example for Bun. Keep the API key server-side.
const url = new URL('/v1/items/stream', process.env.SWARM_API_URL);
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';

const socket = new WebSocket(url, {
  headers: { Authorization: `Bearer ${process.env.SWARM_API_KEY}` },
});

socket.onmessage = ({ data }) => {
  const event = JSON.parse(String(data));
  // Apply the snapshot/delta rules below to your cache.
};
// Add stale-cache handling and reconnect with backoff.
First frame excerpt — inventory.snapshot

Replace your entire scoped cache. The first snapshot also includes changes since an earlier REST read. Pricing and inspect are abbreviated in this example.

{
  "type": "inventory.snapshot",
  "observedAt": 1791460800000,
  "items": [
    {
      "bot_id": "bot-eu-1",
      "asset_id": "30922345678",
      "class_id": "1234567890",
      "instance_id": "0",
      "market_hash_name": "AWP | Asiimov (Field-Tested)",
      "state": "free",
      "first_seen_at": 1791374400000,
      "last_seen_at": 1791460795000,
      "raw": {
        "appid": 730,
        "contextid": "2",
        "name": "AWP | Asiimov (Field-Tested)",
        "tags": [
          {
            "category": "Type",
            "internal_name": "CSGO_Type_SniperRifle"
          }
        ],
        "asset_properties": [
          {
            "propertyid": 2,
            "float_value": 0.27
          },
          {
            "propertyid": 1,
            "int_value": 42
          }
        ]
      },
      "present": true,
      "tradable": true,
      "amount": 1,
      "protected_until": null,
      "settlement_trade_id": null,
      "itemType": "CSGO_Type_SniperRifle",
      "receiveAllowed": true,
      "inspect": {
        "status": "partial",
        "source": "asset_properties",
        "floatValue": 0.27,
        "paintSeed": 42
      },
      "pricing": {
        "currency": "USD",
        "buy": {
          "price": 89,
          "confidence": "medium",
          "validUntil": "2026-10-08T14:00:00.000Z"
        },
        "sell": {
          "price": 84,
          "confidence": "medium",
          "validUntil": "2026-10-08T14:00:00.000Z"
        }
      }
    }
  ],
  "streamId": "d36b8b58-96e4-43ca-b5dc-df56f63cbe10",
  "sequence": 0
}
Next frame — inventory.delta

This example removes one item from the available pool, for example because it was reserved.

{
  "type": "inventory.delta",
  "streamId": "d36b8b58-96e4-43ca-b5dc-df56f63cbe10",
  "sequence": 1,
  "observedAt": 1791460801000,
  "added": [],
  "removed": [
    {
      "bot_id": "bot-eu-1",
      "asset_id": "30922345678"
    }
  ],
  "updated": []
}
Idle connection — inventory.heartbeat
{
  "type": "inventory.heartbeat",
  "streamId": "d36b8b58-96e4-43ca-b5dc-df56f63cbe10",
  "sequence": 1,
  "observedAt": 1791460816000
}
  • added contains full records for items that became available, including released reservations and verified unlocks.
  • removed contains bot_id and asset_id for items that left the available pool.
  • updated contains full replacements when an available item’s quantity, metadata, pricing or receive policy changes. Routine last-seen refreshes alone do not emit updates.

Keep your cache consistent

  1. Wait for the first snapshot, replace the cache and save its streamId and sequence. The initial sequence need not be zero.
  2. Apply each delta as one batch. Its stream ID must match and its sequence must be exactly the previous value plus one. Heartbeats preserve the current sequence.
  3. On a gap, changed stream ID or disconnect, mark the cache stale and reconnect with bounded exponential backoff and jitter. Wait for the new snapshot before treating it as current. Never overwrite an active stream with a late REST response.

The WebSocket sends its own snapshot on every connection, so a separate REST call is optional. There is no historical replay cursor. Database state is sampled approximately once per second; intermediate transitions can coalesce. Steam changes appear after the bot’s existing synchronization observes them. observedAt is the database read time, not the last Steam scan.

Stream prices and receive policies are recomputed from cached data approximately every 15 seconds. New items are enriched immediately. This does not trigger a new request to pricing providers; their feed refresh runs separately. Respect each price’s validUntil, confidence and warnings; a missing price remains null.

Heartbeats arrive every 15 seconds while idle. Mark the cache stale if no frame arrives for 45 seconds. Access is checked on every database read. Limits are 5 connections per key and 100 per API process; slow consumers are disconnected and must reconnect for a fresh snapshot.

Close codes: 1001 server shutdown, 1008 revoked access, disabled workspace or an invalid client message, 1011 read failure, 1013 capacity or slow consumer. The stream only sends data to your backend; do not send application messages. Resolve access errors before reconnecting after 1008.

Removal from this pool can mean a reservation or loss of access. Use offer and settlement webhooks to determine trade outcomes and update balances.

CS2 prices and manipulation risk signals

Item prices, inventory valuations and price manipulation risk signals are included in every plan. Paginated inventory, offer responses, the available-inventory snapshot and WebSocket items include pricing.buy and pricing.sell.

GET /v1/bots/bot-eu-1/items?current=true&search=Asiimov&filter=available&sort=name&limit=100&offset=0

buy.price estimates the purchase price from listings. sell.price estimates the sale price from market bids. Both are USD per item before fees, with independent confidence, expiry and risk. Missing Sell data must not fall back to Buy or an assumed discount.

Response excerpt — pricing.buy and pricing.sell

Computed from fictional observations using the current pricing engine. Buy has an inflation warning and a risk index of 25; Sell has no scored signals and an index of 0. Neither value proves manipulation or safety. Market arrays and compatibility aliases are omitted.

{
  "pricing": {
    "currency": "USD",
    "method": "primary-market-mean-v11",
    "marketHashName": "AWP | Asiimov (Field-Tested)",
    "buy": {
      "basis": "market_ask_reference",
      "price": 89,
      "confidence": "medium",
      "warnings": [
        "partial_market_coverage",
        "single_provider",
        "provider_price_inflated"
      ],
      "validUntil": "2026-09-28T12:00:00.000Z",
      "calculation": "offer_weighted_mean",
      "sourcePolicy": "primary",
      "risk": {
        "status": "warning",
        "flaggedMarkets": [
          "buff163"
        ],
        "uncheckedMarkets": [],
        "historyWarningMarkets": [],
        "recentWarningMarkets": [],
        "missingHistoryMarkets": [],
        "missingRecentMarkets": [],
        "score": 25,
        "scoreKind": "heuristic_index",
        "scoreVersion": "manipulation-risk-v1",
        "scoreCoverage": "complete",
        "scoredMarkets": [
          "buff163",
          "youpin"
        ],
        "scoreFactors": [
          {
            "code": "provider_inflation",
            "points": 25,
            "markets": [
              "buff163"
            ]
          }
        ]
      }
    },
    "sell": {
      "basis": "market_bid_reference",
      "price": 84,
      "confidence": "medium",
      "warnings": [
        "partial_market_coverage",
        "single_provider"
      ],
      "validUntil": "2026-09-28T12:00:00.000Z",
      "calculation": "single_quote",
      "sourcePolicy": "primary",
      "risk": {
        "status": "no_flags",
        "flaggedMarkets": [],
        "uncheckedMarkets": [],
        "historyWarningMarkets": [],
        "recentWarningMarkets": [],
        "missingHistoryMarkets": [],
        "missingRecentMarkets": [],
        "score": 0,
        "scoreKind": "heuristic_index",
        "scoreVersion": "manipulation-risk-v1",
        "scoreCoverage": "complete",
        "scoredMarkets": [
          "buff163_buy"
        ],
        "scoreFactors": []
      }
    }
  }
}
Market quote — one entry from pricing.buy.markets

Source quote, observed listing count, calculation weight, historical medians and recent change. Other markets are omitted from this example.

{
  "market": "buff163",
  "provider": "pricempire",
  "basis": "lowest_listing",
  "price": 90,
  "observedPrice": 90,
  "isInflated": true,
  "inflationThresholdPct": 30,
  "priceHistory": {
    "median7d": 90,
    "median30d": 90,
    "median60d": 90,
    "median90d": 90
  },
  "recentChange": {
    "previousPrice": 90,
    "previousQuotedAt": "2026-09-28T09:45:00.000Z",
    "elapsedSeconds": 900,
    "changeUsd": 0,
    "changePct": 0,
    "quantityChangePct": 0,
    "material": false
  },
  "conversion": null,
  "confidence": "medium",
  "includedInReference": true,
  "role": "primary",
  "usedForValuation": true,
  "valuationWeight": 0.5,
  "quantity": 120,
  "quantityReliable": true,
  "quotedAt": "2026-09-28T10:00:00.000Z",
  "fetchedAt": "2026-09-28T10:00:00.000Z",
  "validUntil": "2026-09-28T12:00:00.000Z",
  "warnings": [
    "provider_price_inflated"
  ]
}

Use prices and risk together

  • Read the required direction’s price, confidence, validUntil and warnings. Expired or null prices are unavailable, never free items.
  • risk.score is a 0–100 heuristic index, not a probability. scoreFactors explains its points and scoredMarkets identifies the valuation contributors.
  • scoreCoverage is complete, partial or unavailable. Missing inputs can produce a null score; do not turn it into zero. A positive score with partial coverage is provisional.
  • risk.status describes broader observed warnings: warning, unknown or no_flags. It can warn about an excluded market even when the valuation score is zero.
  • quantity and quantityReliable describe reported listing or bid counts, not completed sales or guaranteed liquidity at that price.

Inflation flags, historical deviations and recent changes are advisory: they do not automatically block a trade. Apply your own price, risk and availability policy before submitting an offer. Base prices exclude individual float, pattern, sticker and charm premiums; unknown variants can withhold the estimate.

Whole-inventory values and catalog

GET /v1/bots and GET /v1/bots/:botId include inventoryValue.buy and inventoryValue.sell. Totals cover cached holdings, including protected items, with priced, unpriced and risk-warning quantities. Partial totals are not full valuations; calculation time does not prove a fresh inventory scan.

Response excerpt — whole-inventory valuation
{
  "botId": "bot-eu-1",
  "inventoryValue": {
    "currency": "USD",
    "totalQuantity": 24,
    "calculatedAt": "2026-09-28T10:00:00.000Z",
    "buy": {
      "total": 2136,
      "pricedQuantity": 24,
      "unpricedQuantity": 0,
      "riskWarningQuantity": 24,
      "validUntil": "2026-09-28T12:00:00.000Z"
    },
    "sell": {
      "total": 2016,
      "pricedQuantity": 24,
      "unpricedQuantity": 0,
      "riskWarningQuantity": 0,
      "validUntil": "2026-09-28T12:00:00.000Z"
    }
  }
}

The dashboard’s Prices view browses the full cached catalog. Its GET /v1/pricing/catalog endpoint is admin-only, not a trade-key endpoint. Offer item prices show current estimates, not a historical execution price.

Create a trade offer

Replace the sample values with your bot, partner and assets. Generate and save a new UUID idempotency key for each intended operation before sending it.

Create a Steam trade offer

POST /v1/bots/bot-eu-1/offers
Authorization: Bearer <TRADE_API_KEY>
Idempotency-Key: 28f68104-3f84-48e7-b5d9-3128d2a48e30
Content-Type: application/json

{
  "partnerSteamId": "76561197960278073",
  "tradeToken": "abcdefgh",
  "webhookUrl": "https://your-service.example/steam-events",
  "itemsToGive": [
    {
      "assetId": "30884250069"
    }
  ],
  "itemsToReceive": [
    {
      "assetId": "30912345678"
    }
  ],
  "message": "Thanks for trading",
  "timeoutSeconds": 600
}

202 Accepted

{
  "botId": "bot-eu-1",
  "requestId": "28f68104-3f84-48e7-b5d9-3128d2a48e30",
  "offerId": "019eed51-bb80-7c41-86b0-385cffec5c68",
  "action": "offer.create",
  "status": "pending"
}

Saved for processing. Track the request or receive signed webhooks. Offer creation includes mobile Steam Guard confirmation; partner acceptance and settlement happen later.

Trade direction and item ownership
TradeitemsToGiveitemsToReceive
DepositEmptyPartner’s assets
WithdrawalBot’s assetsEmpty
SwapBot’s assetsPartner’s assets

Keep Steam and asset IDs as strings. Each asset defaults to one item. At least one side must contain an item, with up to 100 assets per side. The callback URL is required.

The sample explicitly sets timeoutSeconds: 600; the API default is 30 seconds and the allowed range is 1–604800. It bounds queue start, create preflight and offer lifetime. Cancellation timing depends on Steam and bot availability.

Request body — deposit skins into a bot
{
  "partnerSteamId": "76561197960278073",
  "tradeToken": "abcdefgh",
  "webhookUrl": "https://your-service.example/steam-events",
  "itemsToGive": [],
  "itemsToReceive": [
    {
      "assetId": "30912345678"
    }
  ],
  "message": "Deposit",
  "timeoutSeconds": 600
}
Request body — withdraw skins to a user
{
  "partnerSteamId": "76561197960278073",
  "tradeToken": "abcdefgh",
  "webhookUrl": "https://your-service.example/steam-events",
  "itemsToGive": [
    {
      "assetId": "30884250069"
    }
  ],
  "itemsToReceive": [],
  "message": "Withdrawal",
  "timeoutSeconds": 600
}

These are alternative bodies for the same create endpoint. Give each intended trade its own UUID key.

Track requests and retry safely

HTTP 202 means the request is stored for processing. Save the receipt:

{
  "botId": "bot-eu-1",
  "requestId": "28f68104-3f84-48e7-b5d9-3128d2a48e30",
  "offerId": "019eed51-bb80-7c41-86b0-385cffec5c68",
  "action": "offer.create",
  "status": "pending"
}
GET /v1/bots/:botId/requests/:requestId
GET /v1/bots/:botId/offers?limit=100&offset=0
GET /v1/bots/:botId/offers/:tradeOfferId

Request success, partner acceptance and settlement are separate states. The server’s offerId tracks your operation; Steam’s tradeOfferId becomes available after creation.

200 response — successful create request

The stored payload includes defaulted amounts. Successful creation includes mobile confirmation: result.needsConfirmation is false and state is the observed Steam offer state. Success is not partner acceptance or settlement.

{
  "botId": "bot-eu-1",
  "requestId": "28f68104-3f84-48e7-b5d9-3128d2a48e30",
  "offerId": "019eed51-bb80-7c41-86b0-385cffec5c68",
  "action": "offer.create",
  "status": "succeeded",
  "webhookUrl": "https://your-service.example/steam-events",
  "payload": {
    "partnerSteamId": "76561197960278073",
    "tradeToken": "abcdefgh",
    "webhookUrl": "https://your-service.example/steam-events",
    "itemsToGive": [
      {
        "assetId": "30884250069",
        "amount": 1
      }
    ],
    "itemsToReceive": [
      {
        "assetId": "30912345678",
        "amount": 1
      }
    ],
    "message": "Thanks for trading",
    "timeoutSeconds": 600
  },
  "availableAt": 1790589600000,
  "deadlineAt": 1790590200000,
  "createdAt": 1790589600000,
  "startedAt": 1790589602000,
  "finishedAt": 1790589604000,
  "result": {
    "tradeOfferId": "7993038010",
    "state": 2,
    "needsConfirmation": false,
    "expiresAt": 1790590202000,
    "operationKind": "offer"
  }
}
200 response excerpt — offer list with settlement and prices

Match offer_id or trade_offer_id inside the offers array. Pricing is abbreviated and reflects current estimates, not prices when the trade was created.

{
  "offers": [
    {
      "offer_id": "019eed51-bb80-7c41-86b0-385cffec5c68",
      "request_id": "28f68104-3f84-48e7-b5d9-3128d2a48e30",
      "trade_offer_id": "7993038010",
      "trade_id": "90000000001",
      "partner_steam_id": "76561197960278073",
      "state": 3,
      "is_our_offer": true,
      "items_given": [
        {
          "assetid": "30884250069"
        }
      ],
      "items_received": [
        {
          "assetid": "30912345678",
          "appid": 730,
          "contextid": "2",
          "amount": "1",
          "market_hash_name": "AWP | Asiimov (Field-Tested)",
          "pricing": {
            "currency": "USD",
            "method": "primary-market-mean-v11",
            "marketHashName": "AWP | Asiimov (Field-Tested)",
            "buy": {
              "basis": "market_ask_reference",
              "price": 89,
              "confidence": "medium",
              "warnings": [
                "partial_market_coverage",
                "single_provider",
                "provider_price_inflated"
              ],
              "validUntil": "2026-09-28T12:00:00.000Z",
              "calculation": "offer_weighted_mean",
              "sourcePolicy": "primary",
              "risk": {
                "status": "warning",
                "flaggedMarkets": [
                  "buff163"
                ],
                "uncheckedMarkets": [],
                "historyWarningMarkets": [],
                "recentWarningMarkets": [],
                "missingHistoryMarkets": [],
                "missingRecentMarkets": [],
                "score": 25,
                "scoreKind": "heuristic_index",
                "scoreVersion": "manipulation-risk-v1",
                "scoreCoverage": "complete",
                "scoredMarkets": [
                  "buff163",
                  "youpin"
                ],
                "scoreFactors": [
                  {
                    "code": "provider_inflation",
                    "points": 25,
                    "markets": [
                      "buff163"
                    ]
                  }
                ]
              }
            },
            "sell": {
              "basis": "market_bid_reference",
              "price": 84,
              "confidence": "medium",
              "warnings": [
                "partial_market_coverage",
                "single_provider"
              ],
              "validUntil": "2026-09-28T12:00:00.000Z",
              "calculation": "single_quote",
              "sourcePolicy": "primary",
              "risk": {
                "status": "no_flags",
                "flaggedMarkets": [],
                "uncheckedMarkets": [],
                "historyWarningMarkets": [],
                "recentWarningMarkets": [],
                "missingHistoryMarkets": [],
                "missingRecentMarkets": [],
                "score": 0,
                "scoreKind": "heuristic_index",
                "scoreVersion": "manipulation-risk-v1",
                "scoreCoverage": "complete",
                "scoredMarkets": [
                  "buff163_buy"
                ],
                "scoreFactors": []
              }
            }
          }
        }
      ],
      "created_at": 1790589602000,
      "updated_at": 1788429710000,
      "escrow_end_date": null,
      "accepted_at": 1788429700000,
      "expires_at": 1790590202000,
      "operation_kind": "offer",
      "settlement": {
        "tradeId": "90000000001",
        "state": "trade_protection",
        "steamStatus": 3,
        "initiatedAt": 1788429700000,
        "protectionUntil": 1789034500000,
        "checkedAt": 1788429710000,
        "assetsGiven": [
          {
            "appid": 730,
            "contextid": "2",
            "assetid": "30884250069",
            "new_assetid": "30894250069",
            "new_contextid": "16",
            "amount": "1"
          }
        ],
        "assetsReceived": [
          {
            "appid": 730,
            "contextid": "2",
            "assetid": "30912345678",
            "new_assetid": "30922345678",
            "new_contextid": "16",
            "amount": "1"
          }
        ]
      }
    }
  ]
}
200 response excerpt — one stored offer

GET /v1/bots/:botId/offers/:tradeOfferId returns an offer object. Use the Steam tradeOfferId, not the server’s UUID offerId. Item fields and pricing are abbreviated.

{
  "offer": {
    "offer_id": "019eed51-bb80-7c41-86b0-385cffec5c68",
    "request_id": "28f68104-3f84-48e7-b5d9-3128d2a48e30",
    "trade_offer_id": "7993038010",
    "trade_id": "90000000001",
    "partner_steam_id": "76561197960278073",
    "state": 3,
    "is_our_offer": true,
    "items_given": [
      {
        "assetid": "30884250069"
      }
    ],
    "items_received": [
      {
        "assetid": "30912345678",
        "appid": 730,
        "contextid": "2",
        "amount": "1",
        "market_hash_name": "AWP | Asiimov (Field-Tested)",
        "pricing": {
          "currency": "USD",
          "method": "primary-market-mean-v11",
          "marketHashName": "AWP | Asiimov (Field-Tested)",
          "buy": {
            "basis": "market_ask_reference",
            "price": 89,
            "confidence": "medium",
            "warnings": [
              "partial_market_coverage",
              "single_provider",
              "provider_price_inflated"
            ],
            "validUntil": "2026-09-28T12:00:00.000Z",
            "calculation": "offer_weighted_mean",
            "sourcePolicy": "primary",
            "risk": {
              "status": "warning",
              "flaggedMarkets": [
                "buff163"
              ],
              "uncheckedMarkets": [],
              "historyWarningMarkets": [],
              "recentWarningMarkets": [],
              "missingHistoryMarkets": [],
              "missingRecentMarkets": [],
              "score": 25,
              "scoreKind": "heuristic_index",
              "scoreVersion": "manipulation-risk-v1",
              "scoreCoverage": "complete",
              "scoredMarkets": [
                "buff163",
                "youpin"
              ],
              "scoreFactors": [
                {
                  "code": "provider_inflation",
                  "points": 25,
                  "markets": [
                    "buff163"
                  ]
                }
              ]
            }
          },
          "sell": {
            "basis": "market_bid_reference",
            "price": 84,
            "confidence": "medium",
            "warnings": [
              "partial_market_coverage",
              "single_provider"
            ],
            "validUntil": "2026-09-28T12:00:00.000Z",
            "calculation": "single_quote",
            "sourcePolicy": "primary",
            "risk": {
              "status": "no_flags",
              "flaggedMarkets": [],
              "uncheckedMarkets": [],
              "historyWarningMarkets": [],
              "recentWarningMarkets": [],
              "missingHistoryMarkets": [],
              "missingRecentMarkets": [],
              "score": 0,
              "scoreKind": "heuristic_index",
              "scoreVersion": "manipulation-risk-v1",
              "scoreCoverage": "complete",
              "scoredMarkets": [
                "buff163_buy"
              ],
              "scoreFactors": []
            }
          }
        }
      }
    ],
    "created_at": 1790589602000,
    "updated_at": 1788429710000,
    "escrow_end_date": null,
    "accepted_at": 1788429700000,
    "expires_at": 1790590202000,
    "operation_kind": "offer",
    "settlement": {
      "tradeId": "90000000001",
      "state": "trade_protection",
      "steamStatus": 3,
      "initiatedAt": 1788429700000,
      "protectionUntil": 1789034500000,
      "checkedAt": 1788429710000,
      "assetsGiven": [
        {
          "appid": 730,
          "contextid": "2",
          "assetid": "30884250069",
          "new_assetid": "30894250069",
          "new_contextid": "16",
          "amount": "1"
        }
      ],
      "assetsReceived": [
        {
          "appid": 730,
          "contextid": "2",
          "assetid": "30912345678",
          "new_assetid": "30922345678",
          "new_contextid": "16",
          "amount": "1"
        }
      ]
    }
  }
}

If the HTTP response is lost, retry with the same bot, route, idempotency key and payload. Changing the key creates a new operation and can duplicate the trade. For HTTP 429, respect Retry-After.

Confirmation, cancellation and incoming offers

POST /v1/bots/:botId/offers/:tradeOfferId/confirm
POST /v1/bots/:botId/offers/:tradeOfferId/cancel
POST /v1/bots/:botId/offers/:tradeOfferId/accept
POST /v1/bots/:botId/offers/:tradeOfferId/decline

Use Steam’s tradeOfferId and a new idempotency key per action. Confirm, cancel and decline accept {} or a timeout-only body. Incoming offers, including gifts, are automatically declined. The compatibility /accept route queues requests but they are rejected with INCOMING_OFFERS_DISABLED; receive items through outgoing offers with itemsToReceive.

Creation automatically confirms the bot’s mobile Steam Guard confirmation. The request stays running or reconciling until confirmation is observed. /confirm remains a manual recovery endpoint for outgoing offers; the partner still needs to accept. Email confirmation is not automated and can produce STEAM_CONFIRMATION_REQUIRED. Inspect the existing offer before retrying.

POST /v1/bots/bot-eu-1/offers/7993038010/confirm
Authorization: Bearer <TRADE_API_KEY>
Idempotency-Key: 8811716e-45eb-4da3-a942-8c54d2e7cda2
Content-Type: application/json

{
  "timeoutSeconds": 60
}

The confirmation request above is for manual recovery. Cancel and decline use the same timeout-only body. Cancellation can lose a race with acceptance; follow the observed offer and settlement state.

Signed trade webhooks

Offers created through the API or dashboard send lifecycle events to their saved webhookUrl:

  • request.completed — operation succeeded, was rejected or expired.
  • request.attention-required — an uncertain outcome needs attention.
  • offer.updated — Steam offer state changed.
  • offer.settlement-updated — settlement, protection or reversal changed.

Delivery headers and JSON payloads

POST /steam-events HTTP/1.1
Content-Type: application/json
X-Webhook-Event-Id: 1c2c8862-e3e6-4be5-b107-5c71056dd293
X-Webhook-Timestamp: 1790589605000
X-Webhook-Signature: <64-character hexadecimal HMAC-SHA256>

The headers match the first example; the signature placeholder must be replaced by the sender’s computed signature. Every event includes version, eventId, type, botId, offerId and a millisecond timestamp.

request.completed — succeeded

The swap was created and mobile-confirmed. It is active, but the partner has not accepted and it has not settled.

{
  "version": 1,
  "botId": "bot-eu-1",
  "offerId": "019eed51-bb80-7c41-86b0-385cffec5c68",
  "requestId": "28f68104-3f84-48e7-b5d9-3128d2a48e30",
  "eventId": "1c2c8862-e3e6-4be5-b107-5c71056dd293",
  "type": "request.completed",
  "action": "offer.create",
  "status": "succeeded",
  "tradeOfferId": "7993038010",
  "timestamp": 1790589604000,
  "result": {
    "tradeOfferId": "7993038010",
    "state": 2,
    "needsConfirmation": false,
    "expiresAt": 1790590202000,
    "operationKind": "offer"
  }
}
request.completed — expired

Alternative outcome: the request expired before Steam work began. No Steam offer ID exists.

{
  "version": 1,
  "botId": "bot-eu-1",
  "offerId": "019eed51-bb80-7c41-86b0-385cffec5c68",
  "requestId": "28f68104-3f84-48e7-b5d9-3128d2a48e30",
  "eventId": "b011d929-c532-4557-9f37-a833498fb57b",
  "type": "request.completed",
  "action": "offer.create",
  "status": "expired",
  "timestamp": 1790590200000,
  "error": {
    "code": "REQUEST_EXPIRED",
    "message": "Request deadline elapsed before Steam work started"
  }
}
request.attention-required

Alternative outcome: Steam may have created the offer. Keep the original request; do not create a replacement trade.

{
  "version": 1,
  "botId": "bot-eu-1",
  "offerId": "019eed51-bb80-7c41-86b0-385cffec5c68",
  "requestId": "28f68104-3f84-48e7-b5d9-3128d2a48e30",
  "eventId": "bc770dc6-29cf-490c-8e70-c9e1c19ef301",
  "type": "request.attention-required",
  "action": "offer.create",
  "status": "reconciling",
  "timestamp": 1790590502000,
  "error": {
    "code": "OUTCOME_UNKNOWN",
    "message": "Steam outcome remains unknown; do not retry the mutation automatically"
  }
}
offer.updated — active

The offer moved from needs-confirmation (9) to active (2). The partner still needs to accept.

{
  "version": 1,
  "botId": "bot-eu-1",
  "offerId": "019eed51-bb80-7c41-86b0-385cffec5c68",
  "requestId": "28f68104-3f84-48e7-b5d9-3128d2a48e30",
  "eventId": "d82b6ffd-2644-4358-bcc6-475c91ef0f08",
  "type": "offer.updated",
  "tradeOfferId": "7993038010",
  "state": 2,
  "previousState": 9,
  "revision": 2,
  "timestamp": 1790589610000
}
offer.settlement-updated — trade protection

Steam accepted the offer (state 3), but the exchange is still protected. All timestamps are Unix milliseconds.

{
  "version": 1,
  "botId": "bot-eu-1",
  "offerId": "019eed51-bb80-7c41-86b0-385cffec5c68",
  "requestId": "28f68104-3f84-48e7-b5d9-3128d2a48e30",
  "eventId": "8c4c347d-23a4-43f3-8d14-a1f8de735e81",
  "type": "offer.settlement-updated",
  "tradeOfferId": "7993038010",
  "tradeId": "90000000001",
  "state": 3,
  "revision": 4,
  "timestamp": 1788429710000,
  "settlement": {
    "tradeId": "90000000001",
    "state": "trade_protection",
    "steamStatus": 3,
    "initiatedAt": 1788429700000,
    "protectionUntil": 1789034500000,
    "checkedAt": 1788429710000,
    "assetsGiven": [
      {
        "appid": 730,
        "contextid": "2",
        "assetid": "30884250069",
        "new_assetid": "30894250069",
        "new_contextid": "16",
        "amount": "1"
      }
    ],
    "assetsReceived": [
      {
        "appid": 730,
        "contextid": "2",
        "assetid": "30912345678",
        "new_assetid": "30922345678",
        "new_contextid": "16",
        "amount": "1"
      }
    ]
  }
}
offer.settlement-updated — completed

Protection ended, Steam was checked again and the received asset IDs were verified in inventory.

{
  "version": 1,
  "botId": "bot-eu-1",
  "offerId": "019eed51-bb80-7c41-86b0-385cffec5c68",
  "requestId": "28f68104-3f84-48e7-b5d9-3128d2a48e30",
  "eventId": "c231b394-c99c-472a-a096-65cdce8bcd16",
  "type": "offer.settlement-updated",
  "tradeOfferId": "7993038010",
  "tradeId": "90000000001",
  "state": 3,
  "revision": 5,
  "timestamp": 1789034600000,
  "settlement": {
    "tradeId": "90000000001",
    "state": "completed",
    "steamStatus": 3,
    "initiatedAt": 1788429700000,
    "protectionUntil": 1789034500000,
    "checkedAt": 1789034600000,
    "assetsGiven": [
      {
        "appid": 730,
        "contextid": "2",
        "assetid": "30884250069",
        "new_assetid": "30894250069",
        "new_contextid": "16",
        "amount": "1"
      }
    ],
    "assetsReceived": [
      {
        "appid": 730,
        "contextid": "2",
        "assetid": "30912345678",
        "new_assetid": "30922345678",
        "new_contextid": "16",
        "amount": "1"
      }
    ],
    "inventoryCheckedAt": 1789034550000,
    "missingAssetIds": []
  }
}
offer.settlement-updated — reversed

Alternative to completion: Steam rolled the exchange back during protection. Returned items can receive new asset IDs again.

{
  "version": 1,
  "botId": "bot-eu-1",
  "offerId": "019eed51-bb80-7c41-86b0-385cffec5c68",
  "requestId": "28f68104-3f84-48e7-b5d9-3128d2a48e30",
  "eventId": "156c93f2-af5f-4be6-868b-784a2f8a7743",
  "type": "offer.settlement-updated",
  "tradeOfferId": "7993038010",
  "tradeId": "90000000001",
  "state": 3,
  "revision": 5,
  "timestamp": 1788516100000,
  "settlement": {
    "tradeId": "90000000001",
    "state": "reversed",
    "steamStatus": 6,
    "initiatedAt": 1788429700000,
    "protectionUntil": 1789034500000,
    "checkedAt": 1788516100000,
    "assetsGiven": [
      {
        "appid": 730,
        "contextid": "2",
        "assetid": "30884250069",
        "new_assetid": "30894250069",
        "new_contextid": "16",
        "amount": "1",
        "rollback_new_assetid": "30904250069",
        "rollback_new_contextid": "2"
      }
    ],
    "assetsReceived": [
      {
        "appid": 730,
        "contextid": "2",
        "assetid": "30912345678",
        "new_assetid": "30922345678",
        "new_contextid": "16",
        "amount": "1",
        "rollback_new_assetid": "30932345678",
        "rollback_new_contextid": "2"
      }
    ]
  }
}

The success examples follow the swap above. Expiry, attention and reversal show alternative outcomes; completion and reversal are not two final events for the same exchange.

Offer state and settlement state

Outer state is Steam’s numeric offer state: 2 active, 3 accepted, 9 needs confirmation or 11 in escrow. settlement.state describes the exchange lifecycle:

Settlement states and their meaning
StateMeaning
pendingExchange initialized or held in escrow.
trade_protectionAccepted exchange is still protected.
verifyingProtection ended; item or inventory verification is incomplete.
completedPost-protection checks and required received-item verification passed.
reversedSteam rolled back the exchange.
failedSteam reported failure; investigate item disposition.
unknownProvider status is not recognized.

protectionUntil alone is not proof of completion. Received items use new_assetid; reversals can supply rollback_new_assetid. Do not substitute the sender’s original asset ID when the receiver’s ID is missing.

Verify and process each delivery

Verify X-Webhook-Signature as a hexadecimal HMAC-SHA256 over X-Webhook-Timestamp, a literal . and the raw request body, using your webhook secret. Use a constant-time comparison before parsing JSON.

Check the delivery timestamp in milliseconds for freshness, match X-Webhook-Event-Id to the body’s eventId, and deduplicate deliveries by that ID. Validate the event and persist it before returning 2xx.

Track offer and settlement revisions separately so out-of-order deliveries cannot overwrite newer state. Offers discovered directly on Steam have no saved callback; poll their stored state instead.

Errors and rate limits

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json
{
  "error": "API request limit exceeded",
  "code": "API_RATE_LIMITED"
}

The retry delay above is illustrative; follow the header returned by your API. Check HTTP status before parsing an error: some validation errors omit code, and unknown routes may return a non-JSON 404.

Errors after a 202 arrive in the stored request or a webhook as {"error":{"code":"…","message":"…"}}. An uncertain outcome is not a failed trade.