ExamplesPython

Python

Download runnable source files for each task below. The read-only request, purchases and webhook receiver use the Python standard library. Catalogue streaming and WebSockets add two dependencies. All commands run from the directory containing the downloaded files.

Setup

Use Python 3.11+. SQLite is included in the standard library. Keep the generated merchant.sqlite database private and on persistent disk. Use one worker per database; use separate working directories for catalogue and webhook processes so a large catalogue transaction does not delay webhook intake.

Download merchant.py, then set your API key for HTTP tasks:

export TRADEON_API_KEY='mer_live_REPLACE_ME'

The base URL is https://merchant-api.tradeon.market. Store real keys in your secrets manager; do not commit them or the local database. See Authentication.

First request

python3 merchant.py first

Expected: status: 200, with an items array containing at most one listing. An empty array is valid. The command calls GET /api/v1/merchant/items?limit=1 and spends no money.

Place a purchase and poll the result

Download intent.json. It is a synthetic input, not a real recipient or purchasable item. Replace the item ID and trade URL, set your price ceiling, and set custom_id to an existing order ID from your own system (the commands below assume that you changed it to your-order-42). method selects buy, buy-sync or buy-by-name. For by-name, use intent-by-name.json: its required fields are market_hash_name and trade_url, instead of item_id and target_trade_url.

# Save the exact business intent before any POST. This command is local only.
python3 merchant.py prepare intent.json
# Run only when you intend to spend real funds, using the saved custom_id.
python3 merchant.py submit your-order-42

max_price is the maximum you authorize; TradeOn debits the actual purchase price. target_trade_url is required for buy/sync (trade_url for by-name). The optional SteamID on buy/sync must match its partner; the sample validates that relation locally. See purchase method contracts.

After a timeout or another 'kind': 'unknown' result, keep the saved intent and custom_id:

python3 merchant.py recover your-order-42
# If a repeat is needed, reuse the same persisted intent after appropriate backoff.
python3 merchant.py submit your-order-42
# Read the current state for a returned purchase_id.
python3 merchant.py status PURCHASE_ID

recover queries purchases by custom_id. An empty result does not prove that the original POST failed. Ordinary 4xx errors are returned as rejected: inspect error.code before acting. 408, 429 and 5xx require reconciliation/backoff; the script does not automatically submit again.

A repeat of buy/buy-sync returns the existing purchase with current status/balance. During the initial buy-by-name retry cycle, the current attempt can change and a later response can contain a different purchase_id. Track the business order by custom_id and all returned attempts. Re-read a returned purchase periodically or consume webhooks; completed means delivered but can be followed by a protection rollback. See purchase recovery.

Sync the catalogue (full dump)

Download catalog.py beside merchant.py. Install the exact example dependency versions:

python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install ijson==3.4.0 websockets==15.0.1
# One conditional snapshot download:
python3 catalog.py snapshot
# Continuous snapshot → replay → live synchronization:
python3 catalog.py

The source streams decompressed items into a staging table and checks X-Items-Count before atomically replacing the catalogue and its ETag/cursor/epoch. A failed download leaves the previous committed state intact. urllib preserves gzip bytes; the code decompresses them incrementally before parsing. The dump is large and changes in size; allow space for both old and staged state.

The continuous client starts from the dump cursor, leaves hello.cursor uncommitted, applies absolute changes and commits each batch together with its cursor. It sends JSON {"type":"pong"} for application-level pings. Reconnects use the last committed cursor; 4001/epoch changes and engine degradation require a new snapshot. It stops on 4003 so the connection limit can be fixed. Hourly full snapshots refresh attributes not carried by WS. Snapshot retries wait 120 seconds; normal reconnects back off up to 30 seconds.

A 304 preserves the cached snapshot and any newer WS checkpoint. A resync forces a body request, since an old ETag alone cannot restore a catalogue already changed by WS. Missing snapshot cursor/epoch leaves the client using REST snapshots until a usable checkpoint appears. See synchronization guide and all WS frames and limits.

Receive webhooks

Download webhook_server.py, send_test_webhook.py and webhooks.json beside merchant.py. Start a local receiver using the fixture’s explicitly test-only secret:

export TRADEON_WEBHOOK_SECRET='whsec_documentation_test_only'
python3 webhook_server.py

In another terminal in that directory:

python3 send_test_webhook.py webhooks.json
python3 send_test_webhook.py webhooks.json
python3 merchant.py work

Both sends return 204. The sender checks the fixed HMAC vector, then signs the same body with the current timestamp for the local request. A real receiver must enforce its timestamp window; do not disable it to accept an old fixture.

The receiver verifies raw bytes, timestamp and matching header/payload IDs, commits an inbox entry, and only then acknowledges. work processes pending entries with a separate transaction: event_id deduplicates delivery, (purchase_id, event) deduplicates purchase effects, and provider_uuid deduplicates deposits even if event IDs differ. Accepted duplicates also return 204. Unknown event types are retained and acknowledged without applying a business effect.

effects is a local audit ledger, not a merchant-balance calculation. Purchase events enqueue a reconcile entry; fetch the current purchase before updating your own order state, then consume that reconciliation entry in your application’s worker. No state is sorted by occurred_at, and a late rollback remains actionable after completion. External fulfillment or credits need your own idempotent outbox consumer. See signature verification and business versus delivery deduplication.

Pointing the webhook at your local machine

Synthetic tests stay on loopback. For an intentional live integration, expose the receiver through an HTTPS endpoint you control and register it in the merchant cabinet. The local HTTP server is a development example; use a managed server and durable worker for deployment.

Signature failures and ordinary 4xx responses stop automatic delivery retries. Exceptions 408/429, network failures and 5xx follow the bounded retry policy. A successful local fixture needs no real purchase or manual replay.