Appearance
Webhooks
Skinslink sends webhook notifications to your server when deposit or purchase statuses change. This is the recommended way to track transaction progress.
Setup
Configure your webhook URL in your merchant settings. Skinslink will send POST requests to this URL whenever a trade status changes.
You can also override the webhook URL per deposit by passing result_url when creating a deposit — that URL will take priority over the merchant-level setting.
Deposit Webhooks
Payload
json
{
"sign": "b64EncodedSHA256Signature==",
"status": "completed",
"trade_id": 178,
"trade_date": "2026-02-16T12:00:00Z",
"merchant_tx_id": "order-12345",
"steam_id": "76561198338314767",
"offer_id": "6912345678",
"amount": 36.25,
"amount_currency": "usd"
}Fields
| Field | Type | Description |
|---|---|---|
sign | string | Request signature (see Verifying Webhooks below) |
status | string | Trade status: hold, canceled, failed, completed, reverted |
trade_id | integer | Skinslink internal trade ID |
trade_date | string | Trade creation timestamp (ISO 8601) |
merchant_tx_id | string | Your order ID (the value you passed when creating the deposit) |
steam_id | string | User's Steam ID (64-bit, as string) |
offer_id | string | Steam trade offer ID (only present if available) |
amount | number | Trade amount in USD |
amount_currency | string | Currency code (currently always usd) |
Custom Currency in Webhooks
Custom currency fields (custom_currency, custom_currency_multiplier, custom_currency_rate, custom_currency_sum) are not yet included in webhook payloads. Use Deposit Status to retrieve custom currency data for hold/completed trades.
Deposit Status Flow
Webhooks are sent for the highlighted statuses only:
new ──→ pending ──→ active ──→ completed ✉
│ │ │
│ │ ├──→ hold ✉ ──→ completed ✉
│ │ │ │
│ │ │ └──→ reverted ✉
│ │ │
│ │ ├──→ failed ✉
│ │ │
│ │ └──→ canceled ✉
│ │
│ ├──→ failed ✉
│ │
│ └──→ canceled ✉
│
└──→ canceled ✉- hold — trade is on Steam trade hold (7-day hold)
- completed — items received, deposit credited
- failed — trade failed (user declined, items unavailable, etc.)
- canceled — trade was canceled
- reverted — a previously held trade has been reversed
TIP
Statuses new, pending, and active do not trigger webhook notifications. Use polling if you need to track early status transitions.
Verifying Webhooks
Why verify?
Your webhook endpoint is a public URL — anyone who discovers it could send fake requests pretending to be Skinslink (e.g. "deposit completed, credit the user $50"). The sign field lets you cryptographically prove the webhook actually came from us and hasn't been tampered with.
Always verify the signature before processing a webhook. Without verification, an attacker could forge deposit completions and steal funds.
How it works
The signature is a Base64-encoded SHA-256 hash of your trade_id concatenated with your merchant secret:
sign = base64( sha256( trade_id + secret ) )Your secret is known only to you and Skinslink — so only a legitimate webhook can produce the correct signature for a given trade_id.
Example (Python)
python
import hashlib, base64
expected = base64.b64encode(
hashlib.sha256((str(payload["trade_id"]) + secret).encode()).digest()
).decode()
if expected != payload["sign"]:
return 403 # reject — not from SkinslinkExample (Node.js)
javascript
const crypto = require('crypto');
const expected = crypto
.createHash('sha256')
.update(String(payload.trade_id) + secret)
.digest('base64');
if (expected !== payload.sign) {
return res.status(403).send('Invalid signature');
}Retry Behavior
If your webhook endpoint returns a non-2xx status code, Skinslink will retry delivery with exponential backoff. We recommend:
- Respond with
200 OKas quickly as possible - Process the webhook payload asynchronously
- Implement idempotency using
merchant_tx_id+statusas the deduplication key
Polling as Fallback
If webhooks are not suitable for your setup, you can poll the deposit status endpoint:
bash
curl "https://api.skinslink.com/api/v1/merchant/deposit/status?merchant_tx_id=order-12345" \
-H "X-Api-Key: your-api-key" \
-H "Accept: application/json"We recommend polling no more than once per second per deposit.
Purchase Webhooks
Purchase webhooks are sent to your merchant-level webhook URL when a purchase status changes.
INFO
Purchase webhooks always use your merchant-level webhook URL. There is no per-purchase URL override (unlike deposits, which support result_url).
Payload
json
{
"sign": "b64EncodedSHA256Signature==",
"status": "completed",
"purchase_id": 178,
"trade_date": "2026-03-24T10:30:00Z",
"merchant_tx_id": "order-12345",
"asset_id": "38029384123",
"offer_id": "6912345678",
"amount": 45.99,
"amount_currency": "usd"
}Fields
| Field | Type | Description |
|---|---|---|
sign | string | Request signature (see Verifying Purchase Webhooks below) |
status | string | Purchase status: active, hold, canceled, failed, completed, reverted |
fail_reason | string | Failure reason (only present when status is failed, see Create Purchase) |
purchase_id | integer | Skinslink purchase ID |
trade_date | string | Purchase creation timestamp (ISO 8601) |
merchant_tx_id | string | Your order ID (the value you passed when creating the purchase) |
asset_id | string | Item asset ID (only present if available) |
offer_id | string | Steam trade offer ID (only present if available) |
amount | number | Purchase amount in USD |
amount_currency | string | Currency code (currently always usd) |
Purchase Status Flow
Webhooks are sent for the highlighted statuses only:
new ──→ pending ──→ active ✉ ──→ completed ✉
│ │ │
│ │ ├──→ hold ✉ ──→ completed ✉
│ │ │ │
│ │ │ └──→ reverted ✉
│ │ │
│ │ ├──→ failed ✉
│ │ │
│ │ └──→ canceled ✉
│ │
│ ├──→ failed ✉
│ │
│ └──→ canceled ✉
│
└──→ canceled ✉- active — trade offer created and sent to the user
- hold — trade is on Steam trade hold (7-day hold). Rust items may enter hold for up to 1 hour before completing.
- completed — items successfully transferred
- failed — purchase failed (see
fail_reasonfor details) - canceled — purchase was canceled
- reverted — a previously held purchase has been reversed
TIP
Statuses new and pending do not trigger webhook notifications. Use polling if you need to track early status transitions.
Verifying Purchase Webhooks
The signature is a Base64-encoded SHA-256 hash of your purchase_id concatenated with your merchant secret:
sign = base64( sha256( purchase_id + secret ) )WARNING
purchase_id is an integer — convert it to a string before hashing.
Example (Python)
python
import hashlib, base64
expected = base64.b64encode(
hashlib.sha256((str(payload["purchase_id"]) + secret).encode()).digest()
).decode()
if expected != payload["sign"]:
return 403 # reject — not from SkinslinkExample (Node.js)
javascript
const crypto = require('crypto');
const expected = crypto
.createHash('sha256')
.update(String(payload.purchase_id) + secret)
.digest('base64');
if (expected !== payload.sign) {
return res.status(403).send('Invalid signature');
}Retry & Idempotency
Same retry behavior as deposit webhooks — exponential backoff on non-2xx responses. Deduplicate using merchant_tx_id + status.
Polling as Fallback
bash
curl "https://api.skinslink.com/api/v1/merchant/purchase/status?merchant_tx_id=order-12345" \
-H "X-Api-Key: your-api-key" \
-H "Accept: application/json"