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.

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.

How It Works
- A buyer picks one of your listed items on the SIH market.
- SIH sends
POST /create-purchaseto your server — "accept this order". - Your server creates the purchase record and starts a Steam trade offer.
- SIH starts polling
GET /get-purchase?customId=…every few seconds and keeps polling until the status isfinished(and the whole protection window closes) orfailed. - 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).
| Method | Endpoint path (suggested) | Purpose |
|---|---|---|
| POST | /api/v1/create-purchase | Accept a new order from SIH |
| GET | /api/v1/get-purchase | Return 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:
{
"id": "50231",
"steamId": "76561198000000000",
"token": "SSH18JS",
"price": 1.23,
"customId": "fast-12345"
}| Field | Type | Description |
|---|---|---|
id | string | Your item id (what's being bought; same id you sent in /project/deposit) |
steamId | string | Buyer SteamID64 |
token | string | Buyer trade token (the part after &token= in the tradelink) |
price | number | Order price in USD |
customId | string | Unique 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'orstatus === 'failed' - After
finished— through the wholeprotectionwindow (~7–10 days) untilprotection.statusisfinishedorfailed
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.
{
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):
| Value | When to return |
|---|---|
created | Order accepted, trade not initiated yet |
sent | Trade offer dispatched, offerId is set, awaiting buyer to accept |
finished | Buyer accepted the trade; usually accompanied by a protection block |
failed | Trade did not complete; reason explains why |
reason values (only when status === 'failed'):
| Value | Description |
|---|---|
canceled by buyer | Buyer rejected/cancelled the trade offer |
canceled by seller | Seller (your bot) cancelled the trade offer |
send timeout | Trade offer was not dispatched in time |
bad price | The price in create-purchase is below your minimum |
invalid tradelink | Buyer's tradelink is rejected by Steam |
protection block — see Protection Statuses and Protection Errors for the full state machine.
Important Field Constraints
idmust be a number or a digits-only string. Use1001or"9223372036854775807"(for bigint). Strings with prefixes or letters ("fast-1001") are rejected with awrong-typeschema error.offerIdis camelCase. Notofferid. SIH validators flag the lowercase form aswrong-case.protectionlives insidedata. Putting it at the response root is flagged aswrong-location.- Don't return
reasonfor non-failed statuses. An emptyreason: ""just clutters every response — only include it whenstatus === 'failed'.
Error Responses
| Response body | When 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).
| Endpoint | Payload that's signed |
|---|---|
POST /create-purchase | JSON.stringify(req.body) |
GET /get-purchase | JSON.stringify({ customId }) |
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:
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:
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 payoutFor a rollback during the protection window:
└── { ..., 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
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
POST https://api.sih.market/api/v1/project/user{
"steamId": "76561198000000000",
"tradeToken": "SSH18JS"
}Response 200
{
"success": true,
"data": {
"steamId": "string",
"canTrade": {
"success": "boolean",
"error": "string"
},
"canP2P": {
"success": "boolean",
"error": "string"
}
}
}Response 400
{
"success": false,
"error": "string"
}Get User Info
Authentication Required
GET https://api.sih.market/api/v1/project/user/${steamId}Response 200
{
"success": true,
"data": {
"steamId": "string",
"canTrade": {
"success": "boolean",
"error": "string"
},
"canP2P": {
"success": "boolean",
"error": "string"
}
}
}Get Users List
Authentication Required
GET https://api.sih.market/api/v1/project/usersReturns 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 onlysteamId+online. Max page size 1000.
| Parameter | Type | Description |
|---|---|---|
| limit | number | 1–100 (check=true) | 1–1000 (check=false); default = cap for current mode |
| offset | number | Pagination offset, default 0 |
| check | boolean | true | false, default true |
Response 200 (check=true)
{
"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)
{
"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
{
"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
POST https://api.sih.market/api/v1/project/user/${steamId}/online/${online}Response 200
{
"success": true,
"data": {
"online": "boolean"
}
}Response 400
{
"success": false,
"error": "string"
}Set Users Online (Batch)
Authentication Required
POST https://api.sih.market/api/v1/project/users/online/${online}{
"steamIds": [
"76561198000000000",
"76561198000000001"
]
}Response 200
{
"success": true,
"data": {
"online": true
}
}Response 400
{
"success": false,
"error": "string"
}Deposit Items
Deposit User Items
Authentication Required
POST https://api.sih.market/api/v1/project/deposit| Field | Description |
|---|---|
items[].id | Asset id of the user's item |
items[].price | Price for the user's item |
{
"steamId": "string",
"appId": 0,
"items": [
{
"id": "123",
"price": 0,
"customId": "string",
"amount": 1
}
]
}Response 200
{
"success": true,
"errors": [
"string"
],
"data": [
{
"id": "string",
"item": "string",
"price": 0,
"assetid": "string",
"amount": 1,
"float": 0,
"phase": "string",
"stickers": [
"string"
]
}
]
}Response 400
{
"success": false,
"error": "string",
"data": {
"steamId": "string",
"canTrade": {
"success": true,
"error": "string"
},
"canP2P": {
"success": true,
"error": "string"
}
}
}List Active Deposit Items
Authentication Required
GET https://api.sih.market/api/v1/project/depositsReturns all your deposits that are still on sale — items in status new or paused. The full list is returned in one response (no pagination).
| Field | Description |
|---|---|
id | Our internal item id (the same id returned by /project/deposit and accepted by /project/delete-deposit) |
customId | The custom id you passed on deposit |
price | Item price |
Response 200
{
"success": true,
"data": [
{
"id": "string",
"customId": "string",
"price": 0
}
]
}Response 400
{
"success": false,
"error": "string"
}Delete Deposit User Items
Authentication Required
POST https://api.sih.market/api/v1/project/delete-deposit{
"ids": [
"string"
],
"customIds": [
"string"
]
}Response 200
{
"success": true,
"data": [
"string"
]
}Response 400
{
"success": false,
"error": "string"
}