Authentication
Every request carries four headers. Your key is both the identity and the signing secret; nothing but the signature derived from it travels in the request.
Headers
| Field | Type | Description | |
|---|---|---|---|
| X-Api-Key | string | required | The API key from your console; also the signing secret |
| X-Api-Ts | string | required | Unix milliseconds, within 5 minutes of server time |
| X-Api-Nonce | string | required | Random string of 16-64 chars, unique per request |
| X-Api-Sign | string | required | Base64(HMAC-SHA256(string-to-sign, API key)) |
String to sign
Five segments joined by newlines, in a fixed order. Note that PATH includes the /api prefix, and the query string is included as-is when present.
1METHODUppercase, e.g. GET / POST
2PATH[?QUERY]Includes /api; query verbatim
3X-Api-TsMilliseconds
4X-Api-NonceRandom string
5SHA256_HEX(body)Digest of empty string when no body
Join the five segments with newlines, then X-Api-Sign = Base64(HMAC-SHA256(that string, API key)).
A concrete example
Five segments (newline separated)
GET
/api/open/v1/tasks?from=0&size=5
1758153600000
9f2c1a7b4e6d8035
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855The last line is the SHA-256 of an empty body — the same constant for every GET request, safe to copy verbatim.
Rules
- The timestamp must be within 5 minutes of server time, in either direction. Keep your machine clock in sync.
- The nonce is a random string of 16-64 characters and must differ on every request. Reuse is rejected — this is the replay protection.
- A JSON body is signed via its SHA-256 hex digest. GET requests and direct file uploads use the SHA-256 of an empty string.
- Query parameters are included in their original order — not sorted, not decoded. Use the URL exactly as you request it.
Sample code
# cURL 本身不能算 HMAC,先用 shell 生成签名再发请求
API_KEY="YOUR_API_KEY"
URL="https://api.geeknums.io/api/open/v1/account"
METHOD="GET"
BODY=''
PATH_Q=$(printf '%s' "$URL" | sed -E 's#^https?://[^/]+##')
TS=$(( $(date +%s) * 1000 ))
NONCE=$(head -c16 /dev/urandom | xxd -p)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
SIGN_BASE=$(printf '%s\n%s\n%s\n%s\n%s' "$METHOD" "$PATH_Q" "$TS" "$NONCE" "$BODY_HASH")
SIGN=$(printf '%s' "$SIGN_BASE" | openssl dgst -sha256 -hmac "$API_KEY" -binary | base64)
curl -X "$METHOD" "$URL" \
-H "X-Api-Key: $API_KEY" \
-H "X-Api-Ts: $TS" \
-H "X-Api-Nonce: $NONCE" \
-H "X-Api-Sign: $SIGN"import base64, hashlib, hmac, time, uuid, json
import urllib.request
from urllib.parse import urlsplit
API_KEY = "YOUR_API_KEY"
url = "https://api.geeknums.io/api/open/v1/account"
method = "GET"
body = ""
parts = urlsplit(url)
path = parts.path + (("?" + parts.query) if parts.query else "")
ts = str(int(time.time() * 1000))
nonce = uuid.uuid4().hex
sign_base = "\n".join([method, path, ts, nonce,
hashlib.sha256(body.encode()).hexdigest()])
sign = base64.b64encode(
hmac.new(API_KEY.encode(), sign_base.encode(), hashlib.sha256).digest()
).decode()
headers = {
"X-Api-Key": API_KEY,
"X-Api-Ts": ts,
"X-Api-Nonce": nonce,
"X-Api-Sign": sign,
}
req = urllib.request.Request(url, method=method,
data=body.encode() if body else None, headers=headers)
with urllib.request.urlopen(req) as resp:
print(resp.status, resp.read().decode())import crypto from "node:crypto";
const API_KEY = "YOUR_API_KEY";
const url = "https://api.geeknums.io/api/open/v1/account";
const method = "GET";
const body = "";
const u = new URL(url);
const path = u.pathname + u.search;
const ts = String(Date.now());
const nonce = crypto.randomBytes(16).toString("hex");
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const signBase = [method, path, ts, nonce, bodyHash].join("\n");
const sign = crypto.createHmac("sha256", API_KEY).update(signBase).digest("base64");
const headers = {
"X-Api-Key": API_KEY,
"X-Api-Ts": ts,
"X-Api-Nonce": nonce,
"X-Api-Sign": sign,
};
const res = await fetch(url, { method, headers });
console.log(res.status, await res.text());import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.UUID;
public class OpenApiExample {
public static void main(String[] args) throws Exception {
String apiKey = "YOUR_API_KEY";
String url = "https://api.geeknums.io/api/open/v1/account";
String method = "GET";
String body = "";
URI uri = URI.create(url);
String path = uri.getRawPath() + (uri.getRawQuery() == null ? "" : "?" + uri.getRawQuery());
String ts = String.valueOf(System.currentTimeMillis());
String nonce = UUID.randomUUID().toString().replace("-", "");
MessageDigest sha = MessageDigest.getInstance("SHA-256");
StringBuilder hex = new StringBuilder();
for (byte b : sha.digest(body.getBytes(StandardCharsets.UTF_8))) {
hex.append(String.format("%02x", b));
}
String signBase = String.join("\n", method, path, ts, nonce, hex.toString());
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(apiKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String sign = Base64.getEncoder().encodeToString(
mac.doFinal(signBase.getBytes(StandardCharsets.UTF_8)));
HttpRequest.Builder builder = HttpRequest.newBuilder(uri)
.header("X-Api-Key", apiKey)
.header("X-Api-Ts", ts)
.header("X-Api-Nonce", nonce)
.header("X-Api-Sign", sign)
.method(method, HttpRequest.BodyPublishers.noBody());
HttpResponse<String> res = HttpClient.newHttpClient()
.send(builder.build(), HttpResponse.BodyHandlers.ofString());
System.out.println(res.statusCode() + " " + res.body());
}
}package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
func main() {
apiKey := "YOUR_API_KEY"
rawURL := "https://api.geeknums.io/api/open/v1/account"
method := "GET"
body := ""
u, _ := url.Parse(rawURL)
path := u.EscapedPath()
if u.RawQuery != "" {
path += "?" + u.RawQuery
}
ts := fmt.Sprintf("%d", time.Now().UnixMilli())
nb := make([]byte, 16)
rand.Read(nb)
nonce := hex.EncodeToString(nb)
sum := sha256.Sum256([]byte(body))
signBase := strings.Join([]string{method, path, ts, nonce, hex.EncodeToString(sum[:])}, "\n")
mac := hmac.New(sha256.New, []byte(apiKey))
mac.Write([]byte(signBase))
sign := base64.StdEncoding.EncodeToString(mac.Sum(nil))
req, _ := http.NewRequest(method, rawURL, strings.NewReader(body))
req.Header.Set("X-Api-Key", apiKey)
req.Header.Set("X-Api-Ts", ts)
req.Header.Set("X-Api-Nonce", nonce)
req.Header.Set("X-Api-Sign", sign)
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.StatusCode, string(out))
}