Skip to content

Deposit

There are two ways to integrate item selling into your service: P2P and Polling. Below is how each works.

P2P Integration

With P2P, all transactions go through the SIH p2p system. To sell items, the seller must have the SIH extension installed and grant your project permission to trade.

Deposit

Polling Integration

With Polling, the item shipping status is owned by your system. You implement two REST endpoints on your side; SIH calls them when a buyer places an order and then polls until the trade resolves.

Deposit

How It Works

  1. A buyer picks one of your listed items on the SIH market.
  2. SIH sends POST /create-purchase to your server — "accept this order".
  3. Your server creates the purchase record and starts a Steam trade offer.
  4. SIH starts polling GET /get-purchase?customId=… every few seconds and keeps polling until the status is finished (and the whole protection window closes) or failed.
  5. While polling, you return the current state of the purchase using a strict DTO (see Response DTO).

Reference Implementation

A runnable Node/Express example with HMAC verification, lifecycle simulation and graceful shutdown lives at github.com/devsih/polling-example. It implements both endpoints exactly as described below.

Endpoints

You must implement two endpoints on your partner server. The URLs are configurable from the SIH project settings panel (Purchase URL / Check URL).

MethodEndpoint path (suggested)Purpose
POST/api/v1/create-purchaseAccept a new order from SIH
GET/api/v1/get-purchaseReturn the current state of the purchase

Both calls carry an HMAC-SHA1 signature in the signature header — see Signature.

POST /create-purchase

SIH sends this exactly once per order. If you respond with a network error or success: false, the order is moved to failed immediately.

Request body:

json
{
  "id":       "50231",
  "steamId":  "76561198000000000",
  "token":    "SSH18JS",
  "price":    1.23,
  "customId": "fast-12345"
}
FieldTypeDescription
idstringYour item id (what's being bought; same id you sent in /project/deposit)
steamIdstringBuyer SteamID64
tokenstringBuyer trade token (the part after &token= in the tradelink)
pricenumberOrder price in USD
customIdstringUnique order id from SIH — your idempotency key, save it as-is

Response uses the same DTO as GET /get-purchase.

GET /get-purchase

SIH polls this periodically:

  • Every few seconds until status === 'finished' or status === 'failed'
  • After finished — through the whole protection window (~7–10 days) until protection.status is finished or failed

Query: ?customId=fast-12345 — the same customId SIH sent in create-purchase.

Headers: signature: HMAC-SHA1({customId}, depositApiKey) (hex)

Response DTO

A single schema, with optional fields appearing only in matching states.

ts
{
  success: true,
  data: {
    id:       number,                       // YOUR purchase id (not the item id from request)
    price:    number,
    offerId:  number | null,                // Steam trade offer id; null until status='sent'
    status:   'created' | 'sent' | 'finished' | 'failed',
    avatar:   string,                       // seller-bot avatar URL
    nickname: string,                       // seller-bot nickname

    // Only when status === 'failed':
    reason?:  'canceled by buyer' | 'canceled by seller'
           |  'send timeout' | 'bad price' | 'invalid tradelink',

    // Only when status === 'finished' — protection window:
    protection?: {
      status:          'processing' | 'finished' | 'failed',
      until?:          number,              // unix sec, when the window closes
      error?:          'rollback user' | 'rollback supplier',
      rollbackAt?:     number,              // unix sec, when the trade was rolled back
      rollbackAmount?: number               // how much you refund to the buyer
    }
  }
}

Status values (data.status):

ValueWhen to return
createdOrder accepted, trade not initiated yet
sentTrade offer dispatched, offerId is set, awaiting buyer to accept
finishedBuyer accepted the trade; usually accompanied by a protection block
failedTrade did not complete; reason explains why

reason values (only when status === 'failed'):

ValueDescription
canceled by buyerBuyer rejected/cancelled the trade offer
canceled by sellerSeller (your bot) cancelled the trade offer
send timeoutTrade offer was not dispatched in time
bad priceThe price in create-purchase is below your minimum
invalid tradelinkBuyer's tradelink is rejected by Steam

protection block — see Protection Statuses and Protection Errors for the full state machine.

Important Field Constraints

  • id must be a number or a digits-only string. Use 1001 or "9223372036854775807" (for bigint). Strings with prefixes or letters ("fast-1001") are rejected with a wrong-type schema error.
  • offerId is camelCase. Not offerid. SIH validators flag the lowercase form as wrong-case.
  • protection lives inside data. Putting it at the response root is flagged as wrong-location.
  • Don't return reason for non-failed statuses. An empty reason: "" just clutters every response — only include it when status === 'failed'.

Error Responses

Response bodyWhen to use
{ success: false, error: "invalid signature" }HMAC mismatch on either endpoint
{ success: false, error: "not found" }customId is unknown on your side (only in get-purchase)
{ success: false, error: "bad price" }create-purchase price below minimum
{ success: false, error: "invalid tradelink" }create-purchase payload validation failed

All errors must be returned with HTTP 200 and success: false. Returning 4xx/5xx triggers a network-level failure on the SIH side and may cause unnecessary retries.

Signature

Every call between SIH and your server carries an HMAC-SHA1 signature in the signature header. The shared secret is your project's depositApiKey (generate it in the Sandbox tab of your project settings).

EndpointPayload that's signed
POST /create-purchaseJSON.stringify(req.body)
GET /get-purchaseJSON.stringify({ customId })
js
const crypto = require('node:crypto')

function createSignature(data, apiKey) {
  return crypto
    .createHmac('sha1', apiKey)
    .update(JSON.stringify(data))
    .digest('hex')
}

Verify in constant time

Use crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(expected)) to compare — a naive === leaks timing information that can be used to brute-force the signature byte-by-byte.

Idempotency

SIH may retry POST /create-purchase if the previous call timed out or hit a network error. The retry carries the same customId.

Your handler must detect this and return the existing purchase record instead of creating a duplicate:

js
const existing = await db.purchases.findByCustomId(customId)
if (existing) return res.send({ success: true, data: toDto(existing) })

"not found" Semantics

If customId is unknown on your side, return { success: false, error: "not found" }.

The SIH worker tolerates this for up to 15 minutes — after that, the purchase is marked failed (error: "order is not found"). This protects against permanently-stuck orders if the partner's storage was wiped or the record was never created due to a bug.

Lifecycle Example

For a happy-path order, the polling responses progress through these states:

text
POST /create-purchase
└── { success: true, data: { id: 1001, status: 'created', offerId: null, ... } }

GET /get-purchase (called every few seconds)
├── { ..., status: 'created', offerId: null }                     ← trade not started yet
├── { ..., status: 'sent',     offerId: 1234567890 }              ← offer dispatched
├── { ..., status: 'finished', protection: { status: 'processing', until: 1769740800 } }
│                                                                  ← buyer accepted; protection window opens
└── { ..., status: 'finished', protection: { status: 'finished',  until: 1769136000 } }
                                                                   ← protection closed; seller payout

For a rollback during the protection window:

text
└── { ..., status: 'finished', protection: {
        status:         'failed',
        error:          'rollback user',          // or 'rollback supplier'
        rollbackAt:     1769740800,
        rollbackAmount: 0.98                      // refund to buyer
      }
    }

Testing Your Implementation

The Sandbox tab of your project settings (sandbox.sih.app → your project → Sandbox) ships with:

  • Reachability tests — sends two requests with intentionally broken signatures and verifies you return { success: false, error: "invalid signature" }
  • Live activity log — every real SIH→partner call over the last 24 hours with schema validation and bad-field highlighting
  • DTO Reference — 16 example responses showing what to return in each scenario (happy, failed reasons, rollback variants)

The reference Express implementation at github.com/devsih/polling-example passes all sandbox checks out of the box — clone it as a starting point.

Minimal Express Example

js
const express = require('express')
const bodyParser = require('body-parser')
const crypto = require('node:crypto')

const app = express()
app.use(bodyParser.json())

function createSignature(data, apiKey) {
  return crypto.createHmac('sha1', apiKey).update(JSON.stringify(data)).digest('hex')
}

const depositApiKey = 'YOUR_DEPOSIT_API_KEY'

// POST /api/v1/create-purchase
app.post('/api/v1/create-purchase', async (req, res) => {
  // 1. Verify signature
  if (req.headers.signature !== createSignature(req.body, depositApiKey)) {
    return res.send({ success: false, error: 'invalid signature' })
  }

  const { id, steamId, token, price, customId } = req.body

  // 2. Idempotency — return existing record on retry
  const existing = await db.purchases.findByCustomId(customId)
  if (existing) return res.send({ success: true, data: toDto(existing) })

  // 3. Validate price / tradelink (your business rules)
  if (!(await isPriceValid(id, price))) {
    return res.send({ success: false, error: 'bad price' })
  }

  // 4. Create the purchase and start a Steam trade (async)
  const purchase = await db.purchases.create({
    customId, itemId: id, steamId, token, price, status: 'created',
  })
  void initiateSteamTrade(purchase)

  return res.send({ success: true, data: toDto(purchase) })
})

// GET /api/v1/get-purchase
app.get('/api/v1/get-purchase', async (req, res) => {
  const { customId } = req.query

  if (req.headers.signature !== createSignature({ customId }, depositApiKey)) {
    return res.send({ success: false, error: 'invalid signature' })
  }

  const purchase = await db.purchases.findByCustomId(customId)
  if (!purchase) return res.send({ success: false, error: 'not found' })

  return res.send({ success: true, data: toDto(purchase) })
})

// Internal Purchase → DTO mapping
function toDto(p) {
  const data = {
    id:       p.id,
    price:    p.price,
    offerId:  p.offerId,           // camelCase! Not offerid.
    status:   p.status,
    avatar:   p.bot.avatar,
    nickname: p.bot.nickname,
  }
  if (p.status === 'failed' && p.reason) data.reason = p.reason
  if (p.status === 'finished' && p.protection) data.protection = p.protection
  return data
}

app.listen(3000)

TIP

This is the bare-bones structure. For a full implementation with HMAC constant-time verify, lifecycle simulation, Doppler-phase handling and graceful shutdown — see github.com/devsih/polling-example.

User Management

Add User

Authentication Required

http
POST https://api.sih.market/api/v1/project/user
json
{
  "steamId": "76561198000000000",
  "tradeToken": "SSH18JS"
}

Response 200

json
{
  "success": true,
  "data": {
    "steamId": "string",
    "canTrade": {
      "success": "boolean",
      "error": "string"
    },
    "canP2P": {
      "success": "boolean",
      "error": "string"
    }
  }
}

Response 400

json
{
  "success": false,
  "error": "string"
}

Get User Info

Authentication Required

http
GET https://api.sih.market/api/v1/project/user/${steamId}

Response 200

json
{
  "success": true,
  "data": {
    "steamId": "string",
    "canTrade": {
      "success": "boolean",
      "error": "string"
    },
    "canP2P": {
      "success": "boolean",
      "error": "string"
    }
  }
}

Get Users List

Authentication Required

http
GET https://api.sih.market/api/v1/project/users

Returns a paginated list of bots attached to the project. Two modes:

  • check=true (default) — validates each bot against Steam (canTrade / canP2P). Heavy: one Steam round-trip per user. Max page size 100.
  • check=false — lightweight DB list, returns only steamId + online. Max page size 1000.
ParameterTypeDescription
limitnumber1–100 (check=true) | 1–1000 (check=false); default = cap for current mode
offsetnumberPagination offset, default 0
checkbooleantrue | false, default true

Response 200 (check=true)

json
{
  "success": true,
  "data": [
    {
      "steamId": "76561198000000000",
      "canTrade": {
        "success": true,
        "error": ""
      },
      "canP2P": {
        "success": true,
        "error": ""
      }
    }
  ],
  "pagination": {
    "limit": 100,
    "offset": 0,
    "total": 234,
    "hasMore": true
  }
}

Response 200 (check=false)

json
{
  "success": true,
  "data": [
    {
      "steamId": "76561198000000000",
      "online": true
    }
  ],
  "pagination": {
    "limit": 1000,
    "offset": 0,
    "total": 234,
    "hasMore": false
  }
}

Response 400 — limit exceeds cap, invalid limit/offset, or project not found

json
{
  "success": false,
  "error": "limit 2000 exceeds the cap of 1000 for check=false. Use check=false for larger pages (cap 1000)."
}

Set User Online

Authentication Required

http
POST https://api.sih.market/api/v1/project/user/${steamId}/online/${online}

Response 200

json
{
  "success": true,
  "data": {
    "online": "boolean"
  }
}

Response 400

json
{
  "success": false,
  "error": "string"
}

Set Users Online (Batch)

Authentication Required

http
POST https://api.sih.market/api/v1/project/users/online/${online}
json
{
  "steamIds": [
    "76561198000000000",
    "76561198000000001"
  ]
}

Response 200

json
{
  "success": true,
  "data": {
    "online": true
  }
}

Response 400

json
{
  "success": false,
  "error": "string"
}

Deposit Items

Deposit User Items

Authentication Required

http
POST https://api.sih.market/api/v1/project/deposit
FieldDescription
items[].idAsset id of the user's item
items[].pricePrice for the user's item
json
{
  "steamId": "string",
  "appId": 0,
  "items": [
    {
      "id": "123",
      "price": 0,
      "customId": "string",
      "amount": 1
    }
  ]
}

Response 200

json
{
  "success": true,
  "errors": [
    "string"
  ],
  "data": [
    {
      "id": "string",
      "item": "string",
      "price": 0,
      "assetid": "string",
      "amount": 1,
      "float": 0,
      "phase": "string",
      "stickers": [
        "string"
      ]
    }
  ]
}

Response 400

json
{
  "success": false,
  "error": "string",
  "data": {
    "steamId": "string",
    "canTrade": {
      "success": true,
      "error": "string"
    },
    "canP2P": {
      "success": true,
      "error": "string"
    }
  }
}

List Active Deposit Items

Authentication Required

http
GET https://api.sih.market/api/v1/project/deposits

Returns all your deposits that are still on sale — items in status new or paused. The full list is returned in one response (no pagination).

FieldDescription
idOur internal item id (the same id returned by /project/deposit and accepted by /project/delete-deposit)
customIdThe custom id you passed on deposit
priceItem price

Response 200

json
{
  "success": true,
  "data": [
    {
      "id": "string",
      "customId": "string",
      "price": 0
    }
  ]
}

Response 400

json
{
  "success": false,
  "error": "string"
}

Delete Deposit User Items

Authentication Required

http
POST https://api.sih.market/api/v1/project/delete-deposit
json
{
  "ids": [
    "string"
  ],
  "customIds": [
    "string"
  ]
}

Response 200

json
{
  "success": true,
  "data": [
    "string"
  ]
}

Response 400

json
{
  "success": false,
  "error": "string"
}