Skip to content

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 ​

FieldTypeDescription
signstringRequest signature (see Verifying Webhooks below)
statusstringTrade status: hold, canceled, failed, completed, reverted
trade_idintegerSkinslink internal trade ID
trade_datestringTrade creation timestamp (ISO 8601)
merchant_tx_idstringYour order ID (the value you passed when creating the deposit)
steam_idstringUser's Steam ID (64-bit, as string)
offer_idstringSteam trade offer ID (only present if available)
amountnumberTrade amount in USD
amount_currencystringCurrency code (currently always usd)
hold_end_datestring | nullISO 8601 timestamp for when the Steam hold ends. Only present when status is hold.
fail_reasonstring | nullFailure / cancellation / reversal reason. Present when status is failed, canceled or reverted; on reverted it is user_reverted (the user rolled the trade back) or skinslink_reverted (reversed on our side). See Deposit Status Fail Reasons.

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 Skinslink

Example (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 OK as quickly as possible
  • Process the webhook payload asynchronously
  • Implement idempotency using merchant_tx_id + status as 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",
  "steam_id": "76561198338314767",
  "asset_id": "38029384123",
  "offer_id": "6912345678",
  "amount": 45.99,
  "amount_currency": "usd"
}

Fields ​

FieldTypeDescription
signstringRequest signature (see Verifying Purchase Webhooks below)
statusstringPurchase status: active, hold, canceled, failed, completed, reverted
fail_reasonstring | nullFailure / reversal reason (present when status is failed, canceled or reverted; on reverted, user_reverted or skinslink_reverted), see Purchase Status
purchase_idintegerSkinslink purchase ID
trade_datestringPurchase creation timestamp (ISO 8601)
merchant_tx_idstringYour order ID (the value you passed when creating the purchase)
steam_idstringUser's Steam ID (64-bit, as string)
asset_idstringCatalogue item ID — the same value as item.id in Purchase Status, not a Steam asset ID. See Available Items (only present if available)
offer_idstringSteam trade offer ID (only present if available)
amountnumberPurchase amount in USD
amount_currencystringCurrency code (currently always usd)
hold_end_datestring | nullISO 8601 timestamp for when the hold ends. Set once the purchase enters hold and kept on later statuses. See Hold

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 — item delivered and under Steam trade protection until hold_end_date (CS2: 7 days; Rust: up to 1 hour), see Hold
  • completed — items successfully transferred
  • failed — purchase failed (see fail_reason for 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 Skinslink

Example (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"