生产环境 API 文档
HStore API 文档
Base URL
https://hstore.site/api/v1
版本v1
默认限流60/min
认证
HMAC-SHA256
时间戳窗口
5 分钟
幂等性
24 小时
Webhook 重试
最多 6 次
快速开始
- 在 API Keys 页面为每个集成创建单独的 API 密钥。
- 只在服务端签名。不要在前端代码中暴露 secret。
- 先读取 /catalog,再用 /products/{id} 刷新目标商品,然后再创建订单。
- 在 POST /orders 中始终发送稳定的 external_order_id 和 Idempotency-Key。
- 在重试或超时后,使用 /orders/{id} 或 /orders/lookup 做对账。
认证与请求模型
| 请求头 | 必填 | 说明 |
|---|---|---|
X-API-Key |
是 | 公开的密钥标识。 |
X-Timestamp |
是 | 秒级 Unix 时间戳。必须在 5 分钟窗口内。 |
X-Nonce |
是 | 每次请求都应唯一的随机字符串,用于阻止重放。 |
X-Signature |
是 | 对规范请求计算出的 HMAC-SHA256 小写十六进制签名。 |
Idempotency-Key |
POST /orders | 创建订单时必填。重试同一笔购买时复用它。 |
Origin / Referer |
仅当配置了允许域名时 | 启用域名限制时,必须与某个已配置域名完全匹配。 |
签名规范字符串
METHOD
PATH
QUERY
TIMESTAMP
NONCE
BODY_HASH
规则
- PATH 必须与实际请求路径完全一致,包括 /api/v1。
- QUERY 是原始查询字符串,不包含 ?,顺序必须与发送时一致。
- BODY_HASH 在 GET 时为空,在 POST 时为原始 JSON body 的 SHA256。
- X-Signature 必须是小写十六进制 HMAC-SHA256。
- 如果配置了允许域名,Origin 或 Referer 必须完全匹配。
接口
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/api/v1/ |
API 元数据以及支持的 webhook 事件。 |
GET |
/api/v1/catalog |
按分页和筛选条件列出活动商品。 |
GET |
/api/v1/products/{id} |
在结账前获取完整商品详情。 |
GET |
/api/v1/balance |
读取当前钱包余额和待结算余额。 |
POST |
/api/v1/orders |
使用 Main Wallet 创建并扣款订单。 |
GET |
/api/v1/orders/{id} |
按 HStore 内部订单 ID 读取订单。 |
GET |
/api/v1/orders/lookup?external_order_id=... |
通过你的外部订单号找回订单。 |
示例
API 元数据
curl -X GET 'https://hstore.site/api/v1/'$response = file_get_contents('https://hstore.site/api/v1/');
echo $response;const response = await fetch("https://hstore.site/api/v1/");
console.log(await response.json());import requests
print(requests.get("https://hstore.site/api/v1/").json())Invoke-RestMethod -Uri "https://hstore.site/api/v1/" -Method Get | ConvertTo-Json -Depth 20响应示例
{
"success": true,
"data": {
"name": "HStore Partner API",
"version": "v1",
"docs_url": "https://hstore.site/api-doc",
"webhooks": {
"supported_events": [
"product.stock_changed",
"product.price_changed",
"order.created",
"order.delivered"
]
}
}
}
获取目录列表
API_KEY="hs_pk_xxxxxxxxxxxxxxxx"
API_SECRET="hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
METHOD="GET"
PATH="/api/v1/catalog"
QUERY="page=1&limit=20"
TIMESTAMP=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY_HASH=""
CANONICAL="$METHOD
$PATH
$QUERY
$TIMESTAMP
$NONCE
$BODY_HASH"
SIGNATURE=$(printf "%s" "$CANONICAL" | openssl dgst -sha256 -hmac "$API_SECRET" -hex | sed "s/^.* //")
curl -X GET 'https://hstore.site/api/v1/catalog?page=1&limit=20' \
-H "X-API-Key: $API_KEY" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIGNATURE"$apiKey = 'hs_pk_xxxxxxxxxxxxxxxx';
$apiSecret = 'hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
$method = 'GET';
$path = '/api/v1/catalog';
$query = 'page=1&limit=20';
$body = '';
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$bodyHash = $body === '' ? '' : hash('sha256', $body);
$canonical = implode("\n", [$method, $path, $query, $timestamp, $nonce, $bodyHash]);
$signature = hash_hmac('sha256', $canonical, $apiSecret);
$headers = [
'X-API-Key: ' . $apiKey,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . $signature,
];
$ch = curl_init('https://hstore.site/api/v1/catalog?page=1&limit=20');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;import crypto from "node:crypto";
const apiKey = "hs_pk_xxxxxxxxxxxxxxxx";
const apiSecret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const method = "GET";
const path = "/api/v1/catalog";
const query = "page=1&limit=20";
const rawBody = "";
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(16).toString("hex");
const bodyHash = rawBody ? crypto.createHash("sha256").update(rawBody).digest("hex") : "";
const canonical = [method, path, query, timestamp, nonce, bodyHash].join("\n");
const signature = crypto.createHmac("sha256", apiSecret).update(canonical).digest("hex");
const headers = {
"X-API-Key": apiKey,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
};
const response = await fetch("https://hstore.site/api/v1/catalog?page=1&limit=20", {
method,
headers,
});
console.log(await response.json());import hashlib
import hmac
import json
import secrets
import time
import requests
api_key = "hs_pk_xxxxxxxxxxxxxxxx"
api_secret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
method = "GET"
path = "/api/v1/catalog"
query = "page=1&limit=20"
raw_body = ""
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)
body_hash = hashlib.sha256(raw_body.encode()).hexdigest() if raw_body else ""
canonical = "\n".join([method, path, query, timestamp, nonce, body_hash])
signature = hmac.new(api_secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
headers = {
"X-API-Key": api_key,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
}
response = requests.request(
method,
"https://hstore.site/api/v1/catalog?page=1&limit=20",
headers=headers,
)
print(response.json())$apiKey = "hs_pk_xxxxxxxxxxxxxxxx"
$apiSecret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$method = "GET"
$path = "/api/v1/catalog"
$query = "page=1&limit=20"
$body = ""
$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$nonce = [guid]::NewGuid().ToString("N")
$bodyHash = if ($body -ne "") {
[System.BitConverter]::ToString(([System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes($body)))).Replace("-", "").ToLower()
} else {
""
}
$canonical = "$method`n$path`n$query`n$timestamp`n$nonce`n$bodyHash"
$hmac = [System.Security.Cryptography.HMACSHA256]::new([System.Text.Encoding]::UTF8.GetBytes($apiSecret))
$signature = -join ($hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($canonical)) | ForEach-Object { $_.ToString("x2") })
$headers = @{
"X-API-Key" = $apiKey
"X-Timestamp" = "$timestamp"
"X-Nonce" = $nonce
"X-Signature" = $signature
}
$params = @{
Uri = "https://hstore.site/api/v1/catalog?page=1&limit=20"
Method = $method
Headers = $headers
}
Invoke-RestMethod @params | ConvertTo-Json -Depth 20响应示例
{
"success": true,
"data": {
"items": [
{
"id": 381,
"name": "High-Quality Long-Term (Hotmail) Usable after skipping the first 7 days.",
"slug": "high-quality-long-term-hotmail-usable-after-skipping-the-first-7-days",
"short_description": "High-Quality Long-Term (Hotmail) Usable after skipping the first 7 days.",
"price": 0.02,
"currency": "USD",
"delivery_type": "auto",
"stock_available": 7722,
"product_url": "https://hstore.site/product/381",
"updated_at": "2026-04-10 15:15:29"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 726,
"pages": 37
}
}
}
获取单个商品
API_KEY="hs_pk_xxxxxxxxxxxxxxxx"
API_SECRET="hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
METHOD="GET"
PATH="/api/v1/products/381"
QUERY=""
TIMESTAMP=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY_HASH=""
CANONICAL="$METHOD
$PATH
$QUERY
$TIMESTAMP
$NONCE
$BODY_HASH"
SIGNATURE=$(printf "%s" "$CANONICAL" | openssl dgst -sha256 -hmac "$API_SECRET" -hex | sed "s/^.* //")
curl -X GET 'https://hstore.site/api/v1/products/381' \
-H "X-API-Key: $API_KEY" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIGNATURE"$apiKey = 'hs_pk_xxxxxxxxxxxxxxxx';
$apiSecret = 'hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
$method = 'GET';
$path = '/api/v1/products/381';
$query = '';
$body = '';
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$bodyHash = $body === '' ? '' : hash('sha256', $body);
$canonical = implode("\n", [$method, $path, $query, $timestamp, $nonce, $bodyHash]);
$signature = hash_hmac('sha256', $canonical, $apiSecret);
$headers = [
'X-API-Key: ' . $apiKey,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . $signature,
];
$ch = curl_init('https://hstore.site/api/v1/products/381');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;import crypto from "node:crypto";
const apiKey = "hs_pk_xxxxxxxxxxxxxxxx";
const apiSecret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const method = "GET";
const path = "/api/v1/products/381";
const query = "";
const rawBody = "";
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(16).toString("hex");
const bodyHash = rawBody ? crypto.createHash("sha256").update(rawBody).digest("hex") : "";
const canonical = [method, path, query, timestamp, nonce, bodyHash].join("\n");
const signature = crypto.createHmac("sha256", apiSecret).update(canonical).digest("hex");
const headers = {
"X-API-Key": apiKey,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
};
const response = await fetch("https://hstore.site/api/v1/products/381", {
method,
headers,
});
console.log(await response.json());import hashlib
import hmac
import json
import secrets
import time
import requests
api_key = "hs_pk_xxxxxxxxxxxxxxxx"
api_secret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
method = "GET"
path = "/api/v1/products/381"
query = ""
raw_body = ""
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)
body_hash = hashlib.sha256(raw_body.encode()).hexdigest() if raw_body else ""
canonical = "\n".join([method, path, query, timestamp, nonce, body_hash])
signature = hmac.new(api_secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
headers = {
"X-API-Key": api_key,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
}
response = requests.request(
method,
"https://hstore.site/api/v1/products/381",
headers=headers,
)
print(response.json())$apiKey = "hs_pk_xxxxxxxxxxxxxxxx"
$apiSecret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$method = "GET"
$path = "/api/v1/products/381"
$query = ""
$body = ""
$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$nonce = [guid]::NewGuid().ToString("N")
$bodyHash = if ($body -ne "") {
[System.BitConverter]::ToString(([System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes($body)))).Replace("-", "").ToLower()
} else {
""
}
$canonical = "$method`n$path`n$query`n$timestamp`n$nonce`n$bodyHash"
$hmac = [System.Security.Cryptography.HMACSHA256]::new([System.Text.Encoding]::UTF8.GetBytes($apiSecret))
$signature = -join ($hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($canonical)) | ForEach-Object { $_.ToString("x2") })
$headers = @{
"X-API-Key" = $apiKey
"X-Timestamp" = "$timestamp"
"X-Nonce" = $nonce
"X-Signature" = $signature
}
$params = @{
Uri = "https://hstore.site/api/v1/products/381"
Method = $method
Headers = $headers
}
Invoke-RestMethod @params | ConvertTo-Json -Depth 20响应示例
{
"success": true,
"data": {
"id": 381,
"name": "High-Quality Long-Term (Hotmail) Usable after skipping the first 7 days.",
"slug": "high-quality-long-term-hotmail-usable-after-skipping-the-first-7-days",
"short_description": "High-Quality Long-Term (Hotmail) Usable after skipping the first 7 days.",
"price": 0.02,
"currency": "USD",
"delivery_type": "auto",
"stock_available": 7722,
"product_url": "https://hstore.site/product/381",
"updated_at": "2026-04-10 15:15:29",
"description": "Format includes Hotmail:password. Only supports web login.",
"price_tiers": [
{
"min_quantity": 100,
"unit_price": 0.019
},
{
"min_quantity": 500,
"unit_price": 0.018
}
],
"rules": {
"delivery_type": "auto",
"instant_delivery": true,
"delivery_data_exposed": false
}
}
}
读取余额
API_KEY="hs_pk_xxxxxxxxxxxxxxxx"
API_SECRET="hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
METHOD="GET"
PATH="/api/v1/balance"
QUERY=""
TIMESTAMP=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY_HASH=""
CANONICAL="$METHOD
$PATH
$QUERY
$TIMESTAMP
$NONCE
$BODY_HASH"
SIGNATURE=$(printf "%s" "$CANONICAL" | openssl dgst -sha256 -hmac "$API_SECRET" -hex | sed "s/^.* //")
curl -X GET 'https://hstore.site/api/v1/balance' \
-H "X-API-Key: $API_KEY" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIGNATURE"$apiKey = 'hs_pk_xxxxxxxxxxxxxxxx';
$apiSecret = 'hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
$method = 'GET';
$path = '/api/v1/balance';
$query = '';
$body = '';
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$bodyHash = $body === '' ? '' : hash('sha256', $body);
$canonical = implode("\n", [$method, $path, $query, $timestamp, $nonce, $bodyHash]);
$signature = hash_hmac('sha256', $canonical, $apiSecret);
$headers = [
'X-API-Key: ' . $apiKey,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . $signature,
];
$ch = curl_init('https://hstore.site/api/v1/balance');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;import crypto from "node:crypto";
const apiKey = "hs_pk_xxxxxxxxxxxxxxxx";
const apiSecret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const method = "GET";
const path = "/api/v1/balance";
const query = "";
const rawBody = "";
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(16).toString("hex");
const bodyHash = rawBody ? crypto.createHash("sha256").update(rawBody).digest("hex") : "";
const canonical = [method, path, query, timestamp, nonce, bodyHash].join("\n");
const signature = crypto.createHmac("sha256", apiSecret).update(canonical).digest("hex");
const headers = {
"X-API-Key": apiKey,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
};
const response = await fetch("https://hstore.site/api/v1/balance", {
method,
headers,
});
console.log(await response.json());import hashlib
import hmac
import json
import secrets
import time
import requests
api_key = "hs_pk_xxxxxxxxxxxxxxxx"
api_secret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
method = "GET"
path = "/api/v1/balance"
query = ""
raw_body = ""
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)
body_hash = hashlib.sha256(raw_body.encode()).hexdigest() if raw_body else ""
canonical = "\n".join([method, path, query, timestamp, nonce, body_hash])
signature = hmac.new(api_secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
headers = {
"X-API-Key": api_key,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
}
response = requests.request(
method,
"https://hstore.site/api/v1/balance",
headers=headers,
)
print(response.json())$apiKey = "hs_pk_xxxxxxxxxxxxxxxx"
$apiSecret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$method = "GET"
$path = "/api/v1/balance"
$query = ""
$body = ""
$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$nonce = [guid]::NewGuid().ToString("N")
$bodyHash = if ($body -ne "") {
[System.BitConverter]::ToString(([System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes($body)))).Replace("-", "").ToLower()
} else {
""
}
$canonical = "$method`n$path`n$query`n$timestamp`n$nonce`n$bodyHash"
$hmac = [System.Security.Cryptography.HMACSHA256]::new([System.Text.Encoding]::UTF8.GetBytes($apiSecret))
$signature = -join ($hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($canonical)) | ForEach-Object { $_.ToString("x2") })
$headers = @{
"X-API-Key" = $apiKey
"X-Timestamp" = "$timestamp"
"X-Nonce" = $nonce
"X-Signature" = $signature
}
$params = @{
Uri = "https://hstore.site/api/v1/balance"
Method = $method
Headers = $headers
}
Invoke-RestMethod @params | ConvertTo-Json -Depth 20响应示例
{
"success": true,
"data": {
"balance": 4.887,
"pending_balance": 0,
"currency": "USD"
}
}
创建订单
API_KEY="hs_pk_xxxxxxxxxxxxxxxx"
API_SECRET="hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
METHOD="POST"
PATH="/api/v1/orders"
QUERY=""
BODY='{"product_id":381,"quantity":1,"external_order_id":"shop-order-100045"}'
TIMESTAMP=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY_HASH=$(printf "%s" "$BODY" | openssl dgst -sha256 -hex | sed "s/^.* //")
CANONICAL="$METHOD
$PATH
$QUERY
$TIMESTAMP
$NONCE
$BODY_HASH"
SIGNATURE=$(printf "%s" "$CANONICAL" | openssl dgst -sha256 -hmac "$API_SECRET" -hex | sed "s/^.* //")
IDEMPOTENCY_KEY="idem-shop-order-100045"
curl -X POST 'https://hstore.site/api/v1/orders' \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
--data "$BODY"$apiKey = 'hs_pk_xxxxxxxxxxxxxxxx';
$apiSecret = 'hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
$method = 'POST';
$path = '/api/v1/orders';
$query = '';
$body = '{"product_id":381,"quantity":1,"external_order_id":"shop-order-100045"}';
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$bodyHash = $body === '' ? '' : hash('sha256', $body);
$canonical = implode("\n", [$method, $path, $query, $timestamp, $nonce, $bodyHash]);
$signature = hash_hmac('sha256', $canonical, $apiSecret);
$headers = [
'X-API-Key: ' . $apiKey,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . $signature,
'Content-Type: application/json',
'Idempotency-Key: idem-shop-order-100045',
];
$ch = curl_init('https://hstore.site/api/v1/orders');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $body,
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;import crypto from "node:crypto";
const apiKey = "hs_pk_xxxxxxxxxxxxxxxx";
const apiSecret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const method = "POST";
const path = "/api/v1/orders";
const query = "";
const body = {"product_id":381,"quantity":1,"external_order_id":"shop-order-100045"};
const rawBody = JSON.stringify(body);
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(16).toString("hex");
const bodyHash = rawBody ? crypto.createHash("sha256").update(rawBody).digest("hex") : "";
const canonical = [method, path, query, timestamp, nonce, bodyHash].join("\n");
const signature = crypto.createHmac("sha256", apiSecret).update(canonical).digest("hex");
const headers = {
"X-API-Key": apiKey,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
"Content-Type": "application/json",
"Idempotency-Key": "idem-shop-order-100045",
};
const response = await fetch("https://hstore.site/api/v1/orders", {
method,
headers,
body: rawBody,
});
console.log(await response.json());import hashlib
import hmac
import json
import secrets
import time
import requests
api_key = "hs_pk_xxxxxxxxxxxxxxxx"
api_secret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
method = "POST"
path = "/api/v1/orders"
query = ""
body = {"product_id":381,"quantity":1,"external_order_id":"shop-order-100045"}
raw_body = json.dumps(body, separators=(",", ":"))
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)
body_hash = hashlib.sha256(raw_body.encode()).hexdigest() if raw_body else ""
canonical = "\n".join([method, path, query, timestamp, nonce, body_hash])
signature = hmac.new(api_secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
headers = {
"X-API-Key": api_key,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
"Content-Type": "application/json",
"Idempotency-Key": "idem-shop-order-100045",
}
response = requests.request(
method,
"https://hstore.site/api/v1/orders",
headers=headers,
data=raw_body,
)
print(response.json())$apiKey = "hs_pk_xxxxxxxxxxxxxxxx"
$apiSecret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$method = "POST"
$path = "/api/v1/orders"
$query = ""
$body = '{"product_id":381,"quantity":1,"external_order_id":"shop-order-100045"}'
$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$nonce = [guid]::NewGuid().ToString("N")
$bodyHash = if ($body -ne "") {
[System.BitConverter]::ToString(([System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes($body)))).Replace("-", "").ToLower()
} else {
""
}
$canonical = "$method`n$path`n$query`n$timestamp`n$nonce`n$bodyHash"
$hmac = [System.Security.Cryptography.HMACSHA256]::new([System.Text.Encoding]::UTF8.GetBytes($apiSecret))
$signature = -join ($hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($canonical)) | ForEach-Object { $_.ToString("x2") })
$headers = @{
"X-API-Key" = $apiKey
"X-Timestamp" = "$timestamp"
"X-Nonce" = $nonce
"X-Signature" = $signature
"Idempotency-Key" = "idem-shop-order-100045"
}
$params = @{
Uri = "https://hstore.site/api/v1/orders"
Method = $method
Headers = $headers
Body = $body
ContentType = "application/json"
}
Invoke-RestMethod @params | ConvertTo-Json -Depth 20响应示例
{
"success": true,
"data": {
"id": 16591,
"order_number": "ORD-CFFD69-16591",
"external_order_id": "shop-order-100045",
"status": "delivered",
"quantity": 1,
"unit_price": 0.02,
"total_amount": 0.02,
"currency": "USD",
"delivery_type": "auto",
"delivery": {
"available": true,
"items": [
"[email protected]:password123"
]
},
"links": {
"web_url": "https://hstore.site/order/16591",
"api_url": "https://hstore.site/api/v1/orders/16591"
}
}
}
按 external ID 查询订单
API_KEY="hs_pk_xxxxxxxxxxxxxxxx"
API_SECRET="hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
METHOD="GET"
PATH="/api/v1/orders/lookup"
QUERY="external_order_id=shop-order-100045"
TIMESTAMP=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY_HASH=""
CANONICAL="$METHOD
$PATH
$QUERY
$TIMESTAMP
$NONCE
$BODY_HASH"
SIGNATURE=$(printf "%s" "$CANONICAL" | openssl dgst -sha256 -hmac "$API_SECRET" -hex | sed "s/^.* //")
curl -X GET 'https://hstore.site/api/v1/orders/lookup?external_order_id=shop-order-100045' \
-H "X-API-Key: $API_KEY" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIGNATURE"$apiKey = 'hs_pk_xxxxxxxxxxxxxxxx';
$apiSecret = 'hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
$method = 'GET';
$path = '/api/v1/orders/lookup';
$query = 'external_order_id=shop-order-100045';
$body = '';
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$bodyHash = $body === '' ? '' : hash('sha256', $body);
$canonical = implode("\n", [$method, $path, $query, $timestamp, $nonce, $bodyHash]);
$signature = hash_hmac('sha256', $canonical, $apiSecret);
$headers = [
'X-API-Key: ' . $apiKey,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature: ' . $signature,
];
$ch = curl_init('https://hstore.site/api/v1/orders/lookup?external_order_id=shop-order-100045');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;import crypto from "node:crypto";
const apiKey = "hs_pk_xxxxxxxxxxxxxxxx";
const apiSecret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const method = "GET";
const path = "/api/v1/orders/lookup";
const query = "external_order_id=shop-order-100045";
const rawBody = "";
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(16).toString("hex");
const bodyHash = rawBody ? crypto.createHash("sha256").update(rawBody).digest("hex") : "";
const canonical = [method, path, query, timestamp, nonce, bodyHash].join("\n");
const signature = crypto.createHmac("sha256", apiSecret).update(canonical).digest("hex");
const headers = {
"X-API-Key": apiKey,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
};
const response = await fetch("https://hstore.site/api/v1/orders/lookup?external_order_id=shop-order-100045", {
method,
headers,
});
console.log(await response.json());import hashlib
import hmac
import json
import secrets
import time
import requests
api_key = "hs_pk_xxxxxxxxxxxxxxxx"
api_secret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
method = "GET"
path = "/api/v1/orders/lookup"
query = "external_order_id=shop-order-100045"
raw_body = ""
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)
body_hash = hashlib.sha256(raw_body.encode()).hexdigest() if raw_body else ""
canonical = "\n".join([method, path, query, timestamp, nonce, body_hash])
signature = hmac.new(api_secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
headers = {
"X-API-Key": api_key,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
}
response = requests.request(
method,
"https://hstore.site/api/v1/orders/lookup?external_order_id=shop-order-100045",
headers=headers,
)
print(response.json())$apiKey = "hs_pk_xxxxxxxxxxxxxxxx"
$apiSecret = "hs_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$method = "GET"
$path = "/api/v1/orders/lookup"
$query = "external_order_id=shop-order-100045"
$body = ""
$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$nonce = [guid]::NewGuid().ToString("N")
$bodyHash = if ($body -ne "") {
[System.BitConverter]::ToString(([System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes($body)))).Replace("-", "").ToLower()
} else {
""
}
$canonical = "$method`n$path`n$query`n$timestamp`n$nonce`n$bodyHash"
$hmac = [System.Security.Cryptography.HMACSHA256]::new([System.Text.Encoding]::UTF8.GetBytes($apiSecret))
$signature = -join ($hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($canonical)) | ForEach-Object { $_.ToString("x2") })
$headers = @{
"X-API-Key" = $apiKey
"X-Timestamp" = "$timestamp"
"X-Nonce" = $nonce
"X-Signature" = $signature
}
$params = @{
Uri = "https://hstore.site/api/v1/orders/lookup?external_order_id=shop-order-100045"
Method = $method
Headers = $headers
}
Invoke-RestMethod @params | ConvertTo-Json -Depth 20响应示例
{
"success": true,
"data": {
"id": 16591,
"order_number": "ORD-CFFD69-16591",
"external_order_id": "shop-order-100045",
"status": "delivered",
"quantity": 1,
"unit_price": 0.02,
"total_amount": 0.02,
"currency": "USD",
"delivery_type": "auto",
"delivery": {
"available": true,
"items": [
"[email protected]:password123"
]
},
"links": {
"web_url": "https://hstore.site/order/16591",
"api_url": "https://hstore.site/api/v1/orders/16591"
}
}
}
订单安全
- 每个 POST /orders 都必须同时包含 external_order_id 和 Idempotency-Key。
- 使用相同 payload 重复提交同一个 idempotency key,会返回同一笔订单响应。
- 如果用不同 payload 重复使用同一个 key 或 external_order_id,会返回 409。
- 在不确定是否成功重试后,请用 GET /orders/lookup 对账。
Webhook
Webhook 按密钥分别在 API Keys 页面配置。每个密钥都有自己的 URL、secret、订阅事件和重试队列。
投递请求头
- X-HStore-Event-Id:稳定的事件 ID。
- X-HStore-Delivery-Id:唯一的投递尝试 ID。
- X-HStore-Webhook-Event:事件名称。
- X-HStore-Webhook-Timestamp:Unix 时间戳。
- X-HStore-Signature-Version:当前为 v1。
- X-HStore-Webhook-Signature:小写十六进制 HMAC-SHA256。
投递规则
- 只允许公共 https:// endpoint。
- localhost、loopback 和私有 IP 目标会被拒绝。
- 任意 2xx 响应都会被视为投递成功。
- 非 2xx 响应和网络失败会自动重试。
- 你的接收端应以 X-HStore-Delivery-Id 实现幂等处理。
支持的事件
product.created- Product became availableproduct.updated- Product details updatedproduct.deleted- Product removed or hiddenproduct.stock_changed- Product stock changedproduct.price_changed- Product price changedproduct.tiers_changed- Bulk price tiers changedorder.created- Order createdorder.updated- Order updatedorder.delivered- Order deliveredorder.completed- Order completedorder.refunded- Order refundedorder.disputed- Order disputed
Webhook 载荷示例
{
"id": "evt_9f2d4f7e5c2a4d2abf6f19cd7ad91234",
"type": "product.stock_changed",
"created_at": "2026-04-10T12:30:15Z",
"livemode": true,
"api_version": "v1",
"data": {
"product": {
"id": 381,
"name": "High-Quality Long-Term (Hotmail) Usable after skipping the first 7 days.",
"price": 0.02,
"stock_available": 7700
},
"previous": {
"id": 381,
"stock_available": 7722
},
"changes": {
"stock_available": {
"before": 7722,
"after": 7700
}
},
"meta": {
"source": "purchase"
}
}
}验签规范字符串
TIMESTAMP
DELIVERY_ID
EVENT_ID
EVENT_TYPE
BODY_SHA256
# Webhook signature verification must run inside your app.
# Use this cURL command only to replay a payload into your receiver.
curl -X POST 'https://your-app.example.com/hstore-webhook' \
-H "Content-Type: application/json" \
-H "X-HStore-Webhook-Timestamp: 1712750000" \
-H "X-HStore-Delivery-Id: dlv_xxxxxxxxxxxxxxxx" \
-H "X-HStore-Event-Id: evt_xxxxxxxxxxxxxxxx" \
-H "X-HStore-Webhook-Event: product.stock_changed" \
-H "X-HStore-Webhook-Signature: <hex-hmac-sha256>" \
--data "{\"id\":\"evt_xxx\",\"type\":\"product.stock_changed\"}"$webhookSecret = 'whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
$timestamp = $_SERVER['HTTP_X_HSTORE_WEBHOOK_TIMESTAMP'] ?? '';
$deliveryId = $_SERVER['HTTP_X_HSTORE_DELIVERY_ID'] ?? '';
$eventId = $_SERVER['HTTP_X_HSTORE_EVENT_ID'] ?? '';
$eventType = $_SERVER['HTTP_X_HSTORE_WEBHOOK_EVENT'] ?? '';
$signature = strtolower($_SERVER['HTTP_X_HSTORE_WEBHOOK_SIGNATURE'] ?? '');
$rawBody = file_get_contents('php://input') ?: '';
$bodyHash = hash('sha256', $rawBody);
$canonical = implode("\n", [$timestamp, $deliveryId, $eventId, $eventType, $bodyHash]);
$expected = hash_hmac('sha256', $canonical, $webhookSecret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid webhook signature');
}import crypto from "node:crypto";
const webhookSecret = "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const rawBody = await getRawBody(req);
const timestamp = req.get("X-HStore-Webhook-Timestamp") ?? "";
const deliveryId = req.get("X-HStore-Delivery-Id") ?? "";
const eventId = req.get("X-HStore-Event-Id") ?? "";
const eventType = req.get("X-HStore-Webhook-Event") ?? "";
const signature = (req.get("X-HStore-Webhook-Signature") ?? "").toLowerCase();
const bodyHash = crypto.createHash("sha256").update(rawBody).digest("hex");
const canonical = [timestamp, deliveryId, eventId, eventType, bodyHash].join("\n");
const expected = crypto.createHmac("sha256", webhookSecret).update(canonical).digest("hex");
if (expected !== signature) {
res.status(401).send("Invalid webhook signature");
return;
}import hashlib
import hmac
webhook_secret = "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
raw_body = request.get_data(as_text=True)
timestamp = request.headers.get("X-HStore-Webhook-Timestamp", "")
delivery_id = request.headers.get("X-HStore-Delivery-Id", "")
event_id = request.headers.get("X-HStore-Event-Id", "")
event_type = request.headers.get("X-HStore-Webhook-Event", "")
signature = request.headers.get("X-HStore-Webhook-Signature", "").lower()
body_hash = hashlib.sha256(raw_body.encode()).hexdigest()
canonical = "\n".join([timestamp, delivery_id, event_id, event_type, body_hash])
expected = hmac.new(webhook_secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401, "Invalid webhook signature")$webhookSecret = "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$timestamp = $Request.Headers["X-HStore-Webhook-Timestamp"]
$deliveryId = $Request.Headers["X-HStore-Delivery-Id"]
$eventId = $Request.Headers["X-HStore-Event-Id"]
$eventType = $Request.Headers["X-HStore-Webhook-Event"]
$signature = ($Request.Headers["X-HStore-Webhook-Signature"] ?? "").ToLower()
$rawBody = $Body
$bodyHash = [System.BitConverter]::ToString(([System.Security.Cryptography.SHA256]::Create().ComputeHash([System.Text.Encoding]::UTF8.GetBytes($rawBody)))).Replace("-", "").ToLower()
$canonical = "$timestamp`n$deliveryId`n$eventId`n$eventType`n$bodyHash"
$hmac = [System.Security.Cryptography.HMACSHA256]::new([System.Text.Encoding]::UTF8.GetBytes($webhookSecret))
$expected = -join ($hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($canonical)) | ForEach-Object { $_.ToString("x2") })
if ($expected -ne $signature) {
throw "Invalid webhook signature"
}错误与限流
{
"success": false,
"error": {
"code": "invalid_signature",
"message": "签名与规范请求不匹配。",
"status": 401
}
}限流响应头
- X-RateLimit-Limit:该密钥每分钟允许的最大请求数。
- X-RateLimit-Remaining:当前窗口内剩余请求数。
- X-RateLimit-Reset:当前窗口重置时的 Unix 时间戳。
- Retry-After 只会在 429 时返回。
- 即使中间层把请求头转成小写,头名称也仍然大小写不敏感。
| HTTP | 含义 | 建议操作 |
|---|---|---|
200 |
请求成功。 | 将返回 payload 视为当前真实状态。 |
201 |
订单已创建并返回当前 payload。 | 保存订单 ID 和交付状态。 |
202 |
订单已接受但仍在处理中。 | 轮询订单接口或等待 webhook。 |
400 |
校验失败或请求格式错误。 | 修正请求后再重试。 |
401 |
认证、签名、nonce 或时间戳失败。 | 重新生成请求头并检查签名逻辑。 |
402 |
钱包余额不足。 | 先给 Main Wallet 充值再重试。 |
409 |
重放、请求处理中或 payload 冲突。 | 仅使用同一组购买标识进行安全重试。 |
429 |
已达到限流。 | 等待到 Retry-After 或 X-RateLimit-Reset 后再继续。 |
| 错误代码 | HTTP | 含义 |
|---|---|---|
missing_auth |
401 |
缺少一个或多个必需的认证请求头。 |
request_expired |
401 |
时间戳过旧,或与服务器时间偏差过大。 |
invalid_signature |
401 |
规范字符串或 secret 与请求不匹配。 |
origin_not_allowed |
403 |
Origin 或 Referer 主机与 allowed_domains 不匹配。 |
missing_idempotency_key |
400 |
POST /orders 请求未携带 Idempotency-Key。 |
request_in_progress |
409 |
同一笔购买当前正在处理中。 |
idempotency_conflict |
409 |
同一个 key 或 external_order_id 被用于不同 payload。 |
insufficient_balance |
402 |
钱包余额不足以完成购买。 |
product_unavailable |
404 |
商品不存在、未激活,或卖家未激活。 |
insufficient_stock |
409 |
当前自动发货库存不足。 |
rate_limited |
429 |
该密钥在当前时间窗口内请求过多。 |