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)

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",
  "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_reasonstringFailure reason (only present when status is failed, see Create Purchase)
purchase_idintegerSkinslink purchase ID
trade_datestringPurchase creation timestamp (ISO 8601)
merchant_tx_idstringYour order ID (the value you passed when creating the purchase)
asset_idstringItem asset ID (only present if available)
offer_idstringSteam trade offer ID (only present if available)
amountnumberPurchase amount in USD
amount_currencystringCurrency 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_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"