VS Open Platform POST request signing
Version: V1.0
Revised: 2026-03-16
Scope: All VS Open Platform POST APIs
1. Purpose
Defines how POST requests are signed and verified. Unsigned or invalid signatures are rejected. Covers rules, step-by-step signing, SDK samples, and troubleshooting.
2. Terms
| Term | Definition |
|---|---|
| API Key | Caller id from the portal; env VS_OPEN_API_KEY; sent as X-API-KEY |
| Secret Key | Signing secret; env VS_OPEN_SECRET_KEY; never send over the wire |
| Base URL | Gateway base; env VS_OPEN_API_BASE_URL, e.g. https://api.valuescan.io/api/open/v1 |
| X-TIMESTAMP | 13-digit millisecond timestamp; replay protection |
| X-SIGN | HMAC-SHA256 hex lowercase digest of payload integrity |
| Raw Body | Exact POST body string — no reformatting |
| Path | Relative API path, e.g. /api/v1/order/create, appended to Base URL |
3. Why sign
- Identity: API Key proves an authorized caller
- Integrity: Signature detects body tampering
- Replay: Timestamp bounds validity (default ±5 minutes server-side)
- Safety: Secret never leaves your environment
4. Rules
- POST only; body must be the exact raw string used for signing (JSON, form body, etc.)
- Byte-identical body — no trimming, re-indenting, key reordering
- 13-digit ms timestamp; skew within 5 minutes of server time
- UTF-8 everywhere
- X-SIGN = HMAC-SHA256 hex lowercase (64 hex chars); not Base64 or uppercase
5. Flow
5.1 Overview
flowchart TD
A[Get API Key / Secret / Base URL] --> B[Generate 13-digit ms timestamp]
B --> C[Read raw POST body]
C --> D[signContent = timestamp + raw body]
D --> E[HMAC-SHA256 with Secret Key]
E --> F[Hex digest lowercase]
F --> G[Set X-API-KEY / X-TIMESTAMP / X-SIGN headers]
G --> H[POST to Base URL + Path]5.2 Steps
Step 1 — Configure
From VS Open Platform console, obtain keys and set env vars:
Linux / macOS:
bashexport VS_OPEN_API_KEY="YOUR_API_KEY" export VS_OPEN_SECRET_KEY="YOUR_SECRET_KEY" export VS_OPEN_API_BASE_URL="YOUR_BASE_URL (e.g. https://api.valuescan.io/api/open/v1)"Windows CMD:
cmdset VS_OPEN_API_KEY=YOUR_API_KEY set VS_OPEN_SECRET_KEY=YOUR_SECRET_KEY set VS_OPEN_API_BASE_URL=YOUR_BASE_URL (e.g. https://api.valuescan.io/api/open/v1)
Step 2 — Timestamp
13-digit milliseconds, e.g.:
- Python:
str(int(time.time() * 1000)) - Java:
String.valueOf(System.currentTimeMillis()) - JavaScript:
Date.now().toString()
Step 3 — Raw body
Use the exact string sent in the POST body; if empty, use "".
Step 4 — String to sign
No separator:
signContent = X-TIMESTAMP + RawBodyStep 5 — HMAC-SHA256
Key = Secret Key; message = signContent; output = hex lowercase.
Server reference (Java):
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import java.nio.charset.StandardCharsets;
public static String hmacSign(String content, String hmacKey) {
if (content == null || content.isEmpty() || hmacKey == null || hmacKey.isEmpty()) {
throw new IllegalArgumentException("Content and key required");
}
HMac hMac = new HMac(HmacAlgorithm.HmacSHA256, hmacKey.getBytes(StandardCharsets.UTF_8));
return hMac.digestHex(content);
}Step 6 — Headers
| Header | Value |
|---|---|
| X-API-KEY | VS_OPEN_API_KEY |
| X-TIMESTAMP | From step 2 |
| X-SIGN | From step 5 |
Step 7 — Send
POST to Base URL + Path with the same raw body.
6. SDK examples
6.1 Python
6.1.1 Full example (Base URL + path)
import os
import hmac
import hashlib
import time
import requests
from urllib.parse import urljoin
BASE_URL = os.getenv("VS_OPEN_API_BASE_URL")
if not BASE_URL:
raise EnvironmentError("Set VS_OPEN_API_BASE_URL")
class VSAPISign:
"""Sign and send POST for VS Open Platform."""
@staticmethod
def get_sign_headers(raw_body: str) -> dict:
api_key = os.getenv("VS_OPEN_API_KEY")
secret_key = os.getenv("VS_OPEN_SECRET_KEY")
if not api_key or not secret_key:
raise ValueError(
"Missing VS_OPEN_API_KEY or VS_OPEN_SECRET_KEY\n"
"Linux/Mac: export VS_OPEN_API_KEY='...' && export VS_OPEN_SECRET_KEY='...'\n"
"Windows: set VS_OPEN_API_KEY=... && set VS_OPEN_SECRET_KEY=..."
)
timestamp = str(int(time.time() * 1000))
sign_content = timestamp + raw_body
hmac_obj = hmac.new(
secret_key.encode("utf-8"),
sign_content.encode("utf-8"),
digestmod=hashlib.sha256
)
sign = hmac_obj.hexdigest()
return {
"X-API-KEY": api_key,
"X-TIMESTAMP": timestamp,
"X-SIGN": sign,
"Content-Type": "application/json; charset=utf-8"
}
@staticmethod
def send_post_request(path: str, raw_body: str, timeout: int = 10) -> requests.Response:
full_url = urljoin(BASE_URL, path)
headers = VSAPISign.get_sign_headers(raw_body)
return requests.post(
url=full_url,
headers=headers,
data=raw_body.encode("utf-8"),
timeout=timeout
)
if __name__ == "__main__":
api_path = "/api/v1/order/create"
raw_body = """{
"order_no": "ORD20260316001",
"amount": 100.00,
"product_id": "P10001"
}"""
try:
response = VSAPISign.send_post_request(api_path, raw_body)
print(f"Status: {response.status_code}")
print(f"Body: {response.text}")
except Exception as e:
print(f"Error: {str(e)}")6.1.2 Notes
- Python 3.6+,
pip install requests - Set the three env vars from §5.2.1
- Call
send_post_request(path, raw_body) - Adjust
Content-Typefor non-JSON bodies if needed
6.2 Node.js
6.2.1 Full example
/**
* VS Open Platform POST signing (Node.js)
* Node 16+: built-in crypto, fetch, url
*/
const crypto = require('crypto');
const process = require('process');
const { URL } = require('url');
const BASE_URL = process.env.VS_OPEN_API_BASE_URL;
if (!BASE_URL) {
throw new Error('Set VS_OPEN_API_BASE_URL');
}
function buildSignHeader(rawBody) {
const apiKey = process.env.VS_OPEN_API_KEY;
const secretKey = process.env.VS_OPEN_SECRET_KEY;
if (!apiKey) throw new Error('VS_OPEN_API_KEY not set');
if (!secretKey) throw new Error('VS_OPEN_SECRET_KEY not set');
const timestamp = Date.now().toString();
const signContent = timestamp + rawBody;
const hmac = crypto.createHmac('sha256', secretKey);
hmac.update(signContent, 'utf8');
const sign = hmac.digest('hex');
return {
'X-API-KEY': apiKey,
'X-TIMESTAMP': timestamp,
'X-SIGN': sign,
'Content-Type': 'application/json; charset=utf-8',
'Accept': '*/*'
};
}
async function vsPost(path, data, timeout = 10000) {
const rawBody = typeof data === 'object' ? JSON.stringify(data) : data;
const fullUrl = new URL(path, BASE_URL).href;
const headers = buildSignHeader(rawBody);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(fullUrl, {
method: 'POST',
headers,
body: rawBody,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Timeout (${timeout}ms): ${fullUrl}`);
}
throw new Error(`Request failed: ${error.message}`);
}
}
module.exports = { buildSignHeader, vsPost };
if (require.main === module) {
const apiPath = '/api/v1/order/create';
const requestData = {
order_no: "ORD20260316001",
amount: 100.00,
product_id: "P10001"
};
vsPost(apiPath, requestData)
.then(result => {
console.log('OK:', JSON.stringify(result, null, 2));
})
.catch(error => {
console.error('Error:', error.message);
});
}6.2.2 Notes
- Node 16+
- Env vars (bash/cmd as in Python section)
- Import
vsPost(path, data) - Adjust
Content-Typefor form bodies if needed
7. End-to-end sample
7.1 Inputs
Base URL:
https://api.valuescan.io/api/open/v1API Key:
VS_API_20260316001Secret:
VS_SECRET_8e9f7d6c5b4a3210Path:
/api/v1/order/createBody:
json{ "user_id": "U10001", "action": "create_order", "params": {"goods_id": "G001", "num": 2} }Timestamp:
1710585600000
7.2 Sign payload
Concatenate timestamp + raw JSON (exact spacing):
1710585600000{
"user_id": "U10001",
"action": "create_order",
"params": {"goods_id": "G001", "num": 2}
}Example digest: 5f8d7e6c5b4a32109876543210abcdef5f8d7e6c5b4a32109876543210abcdef
7.3 Request
URL:
https://api.valuescan.io/api/v1/order/createHeaders:
httpX-API-KEY: VS_API_20260316001 X-TIMESTAMP: 1710585600000 X-SIGN: 5f8d7e6c5b4a32109876543210abcdef5f8d7e6c5b4a32109876543210abcdef Content-Type: application/json; charset=utf-8Body: identical raw string used for signing