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": 1788429000000,
      "createdAt": 1788429000000,
      "updatedAt": 1788429600000,
      "itemCount": 24,
      "hosted": true,
      "ownerId": "swarm-eu-1",
      "ownerHeartbeatAt": 1788429600000,
      "loggedIn": true,
      "steamId": "76561198000000001"
    }
  ]
}
200 response — inventory with a protected skin
{
  "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"
    }
  ]
}

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 — partner inventory

This endpoint selects a ready bot. 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"
        }
      ]
    }
  ],
  "nextAssetId": "30912345678"
}

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
}
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 timeoutSeconds: 600 sets the queue-start deadline 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

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. result.needsConfirmation means follow-up is required; succeeded describes offer creation, not 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": 1788429600000,
  "deadlineAt": 1788430200000,
  "createdAt": 1788429600000,
  "startedAt": 1788429602000,
  "finishedAt": 1788429604000,
  "result": {
    "tradeOfferId": "7993038010",
    "needsConfirmation": true,
    "expiresAt": 1788430202000,
    "operationKind": "offer"
  }
}
200 response — offer list with settlement

Match offer_id or trade_offer_id inside the offers array. List rows do not expose webhook revisions, and there is no direct GET-by-offer-ID endpoint.

{
  "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"
        }
      ],
      "created_at": 1788429602000,
      "updated_at": 1788429710000,
      "escrow_end_date": null,
      "accepted_at": 1788429700000,
      "expires_at": 1788430202000,
      "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.

Confirm, cancel, accept or decline

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 {}. Accept requires {"partnerSteamId":"76561197960278073"}.

When the create result reports needsConfirmation, submit mobile confirmation separately. It confirms the bot’s side; the partner must still accept. Email confirmation is not automated.

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
}
Request body — accept an incoming offer

Inspect the incoming offer’s partner and full item set before accepting. Cancel and decline can use the same timeout-only body as confirm.

{
  "partnerSteamId": "76561197960278073",
  "timeoutSeconds": 60
}

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: 1788429605000
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 needs confirmation. It has not been accepted or settled yet.

{
  "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": 1788429604000,
  "result": {
    "tradeOfferId": "7993038010",
    "needsConfirmation": true,
    "expiresAt": 1788430202000,
    "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": 1788430200000,
  "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": 1788430502000,
  "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": 1788429610000
}
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.

Let’s get you started.

Tell us what you need. We’ll reply by email.