Skip to content

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

TermDefinition
API KeyCaller id from the portal; env VS_OPEN_API_KEY; sent as X-API-KEY
Secret KeySigning secret; env VS_OPEN_SECRET_KEY; never send over the wire
Base URLGateway base; env VS_OPEN_API_BASE_URL, e.g. https://api.valuescan.io/api/open/v1
X-TIMESTAMP13-digit millisecond timestamp; replay protection
X-SIGNHMAC-SHA256 hex lowercase digest of payload integrity
Raw BodyExact POST body string — no reformatting
PathRelative 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

  1. POST only; body must be the exact raw string used for signing (JSON, form body, etc.)
  2. Byte-identical body — no trimming, re-indenting, key reordering
  3. 13-digit ms timestamp; skew within 5 minutes of server time
  4. UTF-8 everywhere
  5. X-SIGN = HMAC-SHA256 hex lowercase (64 hex chars); not Base64 or uppercase

5. Flow

5.1 Overview

mermaid
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:

    bash
    export 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:

    cmd
    set 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 + RawBody

Step 5 — HMAC-SHA256

Key = Secret Key; message = signContent; output = hex lowercase.

Server reference (Java):

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

HeaderValue
X-API-KEYVS_OPEN_API_KEY
X-TIMESTAMPFrom step 2
X-SIGNFrom 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)

python
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

  1. Python 3.6+, pip install requests
  2. Set the three env vars from §5.2.1
  3. Call send_post_request(path, raw_body)
  4. Adjust Content-Type for non-JSON bodies if needed

6.2 Node.js

6.2.1 Full example

javascript
/**
 * 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

  1. Node 16+
  2. Env vars (bash/cmd as in Python section)
  3. Import vsPost(path, data)
  4. Adjust Content-Type for form bodies if needed

7. End-to-end sample

7.1 Inputs

  • Base URL: https://api.valuescan.io/api/open/v1

  • API Key: VS_API_20260316001

  • Secret: VS_SECRET_8e9f7d6c5b4a3210

  • Path: /api/v1/order/create

  • Body:

    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/create

  • Headers:

    http
    X-API-KEY: VS_API_20260316001
    X-TIMESTAMP: 1710585600000
    X-SIGN: 5f8d7e6c5b4a32109876543210abcdef5f8d7e6c5b4a32109876543210abcdef
    Content-Type: application/json; charset=utf-8
  • Body: identical raw string used for signing