Authentication
HashNut uses HMAC-SHA256 signatures to authenticate API requests. Some endpoints use header-based signing, while others use an accessSign field in the request body.
HMAC-SHA256 Signing Process
The signing process consists of four steps:
Step 1: Generate Request Metadata
Generate a unique string and a Unix timestamp in milliseconds for each request.
Step 2: Build the Signing String
Concatenate the three components without any separator:
signString = uuid + timestamp + requestBodyWhere requestBody is the JSON string of the request body.
Step 3: Compute the Signature
Compute HMAC-SHA256 using your Secret Key, then Base64-encode the result:
signature = base64( hmac_sha256( secretKey, signString ) )Step 4: Set Request Headers
Include the following headers in your HTTP request:
| Header | Description |
|---|---|
hashnut-request-uuid | The unique string from Step 1; must differ on every request (replays are rejected) |
hashnut-request-timestamp | The Unix timestamp in milliseconds from Step 1; must be within ±5 minutes of server time |
hashnut-request-sign | The Base64-encoded HMAC-SHA256 signature |
Content-Type | Must be application/json |
Code Examples
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.UUID;
public class HashNutSign {
public static String[] sign(String secretKey, String body) throws Exception {
String reqUUID = UUID.randomUUID().toString();
String timestamp = String.valueOf(System.currentTimeMillis()); // milliseconds
String signString = reqUUID + timestamp + body;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secretKey.getBytes(), "HmacSHA256"));
String signature = Base64.getEncoder().encodeToString(
mac.doFinal(signString.getBytes()));
return new String[]{reqUUID, timestamp, signature};
}
}package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"time"
"github.com/google/uuid"
)
func sign(secretKey, body string) (reqUUID, timestamp, signature string) {
reqUUID = uuid.New().String()
timestamp = fmt.Sprintf("%d", time.Now().UnixMilli()) // milliseconds
signString := reqUUID + timestamp + body
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(signString))
signature = base64.StdEncoding.EncodeToString(mac.Sum(nil))
return reqUUID, timestamp, signature
}const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
function sign(secretKey, body) {
const reqUUID = uuidv4();
const timestamp = Date.now().toString(); // milliseconds
const signString = reqUUID + timestamp + body;
const signature = crypto
.createHmac('sha256', secretKey)
.update(signString)
.digest('base64');
return { reqUUID, timestamp, signature };
}import hmac
import hashlib
import base64
import uuid
import time
def sign(secret_key: str, body: str):
req_uuid = str(uuid.uuid4())
timestamp = str(int(time.time() * 1000)) # milliseconds
sign_string = req_uuid + timestamp + body
signature = base64.b64encode(
hmac.new(
secret_key.encode(),
sign_string.encode(),
hashlib.sha256
).digest()
).decode()
return req_uuid, timestamp, signatureAccess Sign (Body-Level Signing)
Query Order and Confirm Payment do not use header signing. They carry accessSign in the request body instead, and it is computed completely differently from the header signature above:
accessSign = HMACSHA256(secretKey, payOrderId and merchantOrderId sorted case-insensitively, then concatenated)The result is an uppercase hexadecimal string (64 characters), not base64.
IMPORTANT
You never need to compute it — Create Order returns it as data.accessSign. Persist it alongside payOrderId and echo it back verbatim on later query and confirm calls. Because it depends only on those two IDs and not on the rest of the body, an order's accessSign is a fixed value.
Endpoint Authentication Methods
| Endpoint | Auth Method | Description |
|---|---|---|
POST /v4.0.0/api/orders/create | Header signing | Create order |
POST /v4.0.0/api/orders/cancel | Header signing | Cancel order |
POST /v4.0.0/api/orders/query | Body accessSign | Query order |
POST /v4.0.0/pay/orders/confirm | Body accessSign | Confirm payment |
POST /v4.0.0/api/orders/supplements | None | Query supplements; only payOrderId format is checked |
POST /v4.0.0/api/orders/supplements/latest | None | Same, most recent record only |
POST /v4.0.0/config/* | None | Public config endpoints |
WARNING
The supplement endpoints perform no authentication — a valid payOrderId is enough to read the data. Do not expose payOrderId on your own public pages or anywhere reachable from your frontend.