Skip to main content

Withdrawals

A withdrawal is when a user buys a skin from the market using their balance on your platform. AssetPay purchases the item from a marketplace supplier and delivers it to the user via Steam trade offer.

The Withdrawal Flow

1

Fetch market

Call GET /client/market to get available items with current prices.
2

User selects an item

Display market items in your UI. The user picks what they want to buy.
3

Initiate withdrawal

The user (or your backend) calls POST /client/trading/withdraw with the selected item details. The trade is created with initiated status.
4

AssetPay calls your backend (initiated callback)

Before purchasing anything, AssetPay sends an initiated callback to your backend. This is where you check the user’s balance and approve or reject the trade.
5

Your backend approves or rejects

If the user has enough balance: deduct it and respond 2xx. AssetPay proceeds with the purchase. If not: respond with 4xx (e.g. 402) and the trade is marked failed.
6

Item sourced and offer sent

AssetPay sources the item from the supplier (pending), then sends the Steam trade offer to the user (active). When the user accepts, the trade moves to hold for the Steam reversal-protection window (and the callback now carries offerID + holdEndDate).
7

Completed

After the hold period, the trade reaches completed. No further action needed.
The key thing here: nothing gets purchased until your backend approves it. The initiated callback is the gate. Your backend is always in control of whether a withdrawal goes through.

Initiating a Withdrawal

POST https://api.assetpay.gg/client/trading/withdraw
Content-Type: application/json
Authorization: CLIENT_TOKEN

{
  "items": [
    { "itemId": "e5f6g7h8-...", "price": 45.00 }
  ],
  "game": "730",
  "externalId": "wd_unique_789"
}

Request Fields

FieldTypeRequiredDescription
itemsarrayYesArray of items to withdraw (min 1, max 50)
items[].itemIdstringYesItem ID from the market response
items[].pricenumberYesPurchase price in USD (max $100,000). Validated against the current marketplace price.
items[].amountnumberNoQuantity for stackable items (default: 1, max: 10000)
gamestringNo"730" (CS2) or "252490" (Rust). Defaults to "730".
externalIdstringNoYour unique tracking ID (max 128 chars)

Response

{
  "requestId": "...",
  "success": true,
  "data": {
    "id": "trade-uuid",
    "type": "withdraw",
    "source": "client",
    "status": "initiated",
    "game": "730",
    "externalId": "wd_unique_789",
    "items": [
      {
        "id": "e5f6g7h8-...",
        "appid": 730,
        "tradable": true,
        "amount": 1,
        "status": "initiated",
        "offer": {
          "price": 45.00,
          "reference": "ref_market_789"
        }
      }
    ],
    "totalPrice": 45.00,
    "createdAt": "2026-03-04T10:00:00.000Z",
    "updatedAt": "2026-03-04T10:00:00.000Z"
  }
}
On a freshly initiated withdraw the items only carry the data you submitted (itemId, offer.price) — Steam-side fields like name, marketHashName, type, and iconUrl are omitted until they resolve.

The initiated callback

This is the most important callback in the entire system. When a user initiates a withdrawal, AssetPay does not start purchasing immediately. Instead, it sends an initiated callback to your backend and waits for your response. The approval callback uses the same { trade: ... } body shape as every other state callback — see Callbacks for handling details and signature format. Your backend should:
  1. Verify the X-AssetPay-Signature header
  2. Look up the user by trade.clientSteamID or trade.externalClientUserId
  3. Check if they have enough balance for trade.totalPrice
  4. If yes: deduct the balance and respond with HTTP 2xx
  5. If no: respond with HTTP 4xx (e.g. 402) to reject

Approving a withdrawal

Respond with 200 and an empty body (or any body that isn’t a rejection signal). AssetPay takes that as approval and starts the purchase.

Rejecting a withdrawal

Respond with any 4xx status code. 402 (Payment Required) is the recommended choice:
HTTP/1.1 402 Payment Required
Content-Type: application/json

{ "reason": "Insufficient balance" }
The reason field is optional but useful for debugging. Any 4xx status (400, 402, 403, 409, etc.) is treated as a rejection. Alternative formats (still supported): A 200 response with one of these bodies is also treated as a rejection:
  • { "action": "reject", "reason": "..." }
  • { "status": "rejected" }
  • { "errorCode": "INSUFFICIENT_BALANCE" }
  • { "code": "INSUFFICIENT_BALANCE" }
All rejection paths mark the trade as failed and refund your merchant wallet automatically. You’ll receive a follow-up failed callback.
5xx responses and timeouts are not treated as rejections. They are treated as delivery failures and retried with the standard webhook backoff. Make sure your rejection logic returns 4xx, not 5xx.

Balance Handling for Withdrawals

trade.statusWhat your backend should do
initiatedCheck user balance, deduct it, respond 2xx to approve. Or reject with 4xx.
pendingSupplier is sourcing the item. Acknowledge with 200. No balance action.
activeSteam trade offer sent to the user, awaiting acceptance. Acknowledge with 200. No balance action.
holdItem sourced and the Steam trade offer accepted by the user, hold running (offerID + holdEndDate present). Acknowledge with 200.
completedDelivery complete. No balance action needed.
failedTrade did not go through. Refund totalPrice if you previously deducted it. The failed item(s) carry an error code — see Per-Item Failure Reasons.
declinedThe user declined / let the Steam offer expire (buyer fault). AssetPay auto-refunds your merchant wallet minus a 2% penalty (capped at $9). Decide your own user-facing refund — refund in full and absorb the 2%, or pass it on.
canceledTrade canceled before processing. Refund totalPrice if you previously deducted it.
revertedA completed withdrawal was reversed. Refund totalPrice. Check revertedBy.
A withdrawal progresses initiated → pending → active → hold → completed. The intermediate statuses (pending, active) are informational; drive your balance logic off initiated (deduct) and the terminal states (completed / failed / declined / canceled / reverted). Rust withdrawals normally skip hold. Rust items have no 7-day reversal window, so a Rust withdrawal usually goes active → completed directly with no holdEndDate (the exception is a Steam security escrow, which surfaces as hold). CS2 withdrawals always pass through hold. Price true-up on completion. If the supplier fills below the price you locked, AssetPay settles the actual cost and refunds the difference (including the fee on it) to your merchant wallet; totalPrice is trued up to the actual spend. You never pay more than your locked price. This applies to both standard and quick withdrawals.

Trade Reversals

In rare cases, a completed withdrawal can be reversed. This happens when either the supplier or the user cancels the Steam trade after acceptance. The trade object includes a revertedBy field:
  • "supplier" — the marketplace supplier reversed the trade
  • "user" — the user declined or cancelled the trade offer
When a withdrawal is reverted, your merchant wallet is automatically refunded by AssetPay.

Quick Withdrawals

If you don’t want to pick specific listings, use quick withdrawal: pass a catalog itemId (or its exact marketHashName instead), a per-unit price ceiling, and an amount, and AssetPay buys the cheapest available listings at or below your ceiling. CS2 only.
POST https://api.assetpay.gg/client/trading/withdraw/quick
Content-Type: application/json
Authorization: CLIENT_TOKEN

{ "itemId": "dc3c4460d814ac35", "maxPrice": 67.39, "amount": 3, "delivery": "instant" }
maxPrice is the gross per-unit ceiling (fee-inclusive — the same withdraw price returned by /secure/prices). It differs from the standard withdrawal in two ways:
  • Pre-filled rows. The response returns one item row per requested unit (same minimal shape as a normal buy — id, ceiling price, delivery), and totalPrice set to the worst-case lock (amount × maxPrice). Each row is bound to a real listing as fills arrive; the trade settles asynchronously — watch via callbacks or the WebSocket feed.
  • Partial fills are normal. If fewer than amount listings are available at or below your ceiling, AssetPay buys what it can; the unfilled rows go to failed, the unfilled units and any below-ceiling savings are refunded to your balance, and totalPrice is trued up to the actual spend once settled.
Everything else — the initiated approval callback, balance locking, per-item status — works exactly like the standard withdrawal. See the API reference for POST /client/trading/withdraw/quick (and the merchant self-trade variant POST /secure/buy/quick).

Cancelling a Withdrawal Item

A user can cancel a withdrawal item that hasn’t been delivered yet — for example when a CS2 purchase is taking too long to source. Call POST /client/trading/withdraw/{tradeId}/items/{itemId}/cancel with the trade ID and the item’s id. Cancellation is CS2 (Assetpay) only — Rust withdrawals use a polling supplier with no cancel API. An item can only be cancelled when it is at least 30 minutes old and not yet in a terminal state (completed, failed, reverted, canceled); otherwise you’ll get TRADE_CANCEL_TOO_SOON (27) or TRADE_NOT_CANCELLABLE (28). A successful response only confirms the marketplace accepted the cancel — AssetPay doesn’t change the item state inline. The item transitions to reverted and your merchant wallet is refunded asynchronously via the usual callback, exactly like a trade reversal. Refund the user’s balance when you receive that reverted callback. See the API reference for the full contract.

Per-Item Status

For multi-item withdrawals, each item in the items array carries its own status field. It uses the same values as the trade-level TradeStatus and mirrors how the individual purchase progresses:
StatusMeaning
initiatedPurchase not started yet
pendingSupplier sourcing the item
activeSteam trade offer sent, awaiting acceptance
holdItem delivered to user, hold period running
completedItem delivered successfully and hold cleared
failedPurchase or delivery failed for this item (carries an error code)
declinedThe user declined the Steam offer for this item
canceledTrade canceled before processing
revertedTrade was reversed for this item
This lets you show per-item progress in your UI for multi-item withdrawals.

Per-Item Failure Reasons

When an item ends in failed, it carries an error field with a stable TradeFailureCode so you can react programmatically:
errorWhat happenedSuggested handling
LISTING_UNAVAILABLEListing sold / delisted / out of stockRetry with another listing
PRICE_CHANGEDPrice moved above your ceilingRe-quote and retry
TRADE_URL_INVALIDRecipient’s Steam trade URL is invalid or escrow-blockedAsk the user to fix their trade URL
STEAM_ACCOUNT_RESTRICTEDRecipient’s Steam can’t receive (VAC / ban / hold / private)User must resolve the Steam restriction
MARKET_UNAVAILABLETemporary upstream / our-side issueRetry later
PURCHASE_FAILEDUnclassified failureReconcile, then retry or surface to the user
The trade-level error carries this code only when the whole withdrawal failed for one shared reason; for mixed outcomes it’s null, so read the per-item error values. canceled and declined items carry no error — the status itself is the reason. The raw upstream detail is kept internal and never surfaced.