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 firstExpected: 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-42max_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_IDrecover 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.pyThe 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.pyIn another terminal in that directory:
python3 send_test_webhook.py webhooks.json
python3 send_test_webhook.py webhooks.json
python3 merchant.py workBoth 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.