API REFERENCE · v1
域名管理 API 文档
提供给客户端 SDK 集成:高速获取域名清单、上报 ping 结果。所有接口基于 HTTPS + HMAC-SHA256 签名。
Base URL
https://www.domain-manager.top
概览
系统将每个网站主体抽象为一个「服务」。每个服务拥有独立的 api_key 与 sign_key,并绑定多个域名。客户端通过:
- 从 CDN URL 高速读取该服务最新的域名清单 JSON
- 客户端 SDK 对清单中的域名进行 ping 测试,上报到本系统
- 后台基于上报数据分析可用性与时延
写入
POST /api/v1/{slug}/ping
上报 ping 结果,需 HMAC 签名
读取
GET /api/v1/{slug}/domains.json
无需鉴权(推荐走 CDN URL)
认证与签名
写入类接口必须携带以下 4 个请求头:
| Header | 说明 |
|---|---|
| X-API-Key | 服务的 api_key,用于识别请求归属 |
| X-Timestamp | Unix 秒级时间戳,与服务端偏差需在 ±300 秒内 |
| X-Nonce | 本次请求的随机字符串(建议 16 字节十六进制),10 分钟内不可复用 |
| X-Signature | 下方算法生成的签名(小写十六进制) |
签名算法
将以下字段按顺序用 \n(即 0x0A)拼接为待签名字符串:
METHOD (大写,例如 POST) PATH (含前导 /,不含 query string) X-Timestamp X-Nonce sha256(BODY) (请求体的 SHA-256 十六进制;GET/无体时使用空串的 sha256)
然后以 sign_key 为密钥做 HMAC-SHA256,输出为小写十六进制,放入 X-Signature。
安全提示:sign_key 仅在创建/重置时显示一次,请妥善保存到客户端配置或密钥管理平台。绝不要将 sign_key 暴露在前端代码或日志中。
错误码
所有错误以 JSON 形式返回:{ "error": "...", "message": "..." }。
| HTTP | error | 含义 |
|---|---|---|
| 401 | unauthorized | 缺少签名头 / 时间戳过期 / nonce 重放 / 签名不匹配 |
| 403 | forbidden | api_key 不属于该 slug |
| 422 | validation_failed | 参数校验失败(详见 message) |
| 429 | rate_limited | 限流触发,Retry-After 提示等待秒数 |
| 404 | not_found | 服务不存在或已停用 |
POST
/api/v1/{slug}/ping
客户端批量上报对域名的 ping 结果。需要签名认证。
请求体
{
"reports": [
{
"host": "api.example.com",
"success": true,
"latency_ms": 42,
"error_code": null,
"ts": 1740000000
}
]
}
| 字段 | 类型 | 说明 |
|---|---|---|
| reports | array(1-100) | 单次最多 100 条 |
| reports[].host | string | 域名(必须出现在该服务的清单中或子域) |
| reports[].success | bool | 本次 ping 是否成功 |
| reports[].latency_ms | int (可选) | 耗时毫秒,0–600000 |
| reports[].error_code | string (可选) | 失败原因短码,如 timeout / dns / refused |
| reports[].ts | int (可选) | 上报对应的 Unix 秒时间戳,缺省取服务端时间 |
响应
200 OK
{ "ok": true, "accepted": 1 }
限流
默认按 service + 客户端 IP 维度,每分钟 120 次(可在系统设置中调整)。超出返回 429。
GET
CDN 域名清单
客户端启动时应从该 URL 拉取最新域名清单。该 URL 由后台「服务详情」页提供,托管在 CDN 上,全球高速访问。
GET https://<your-cdn>/services/<slug>/domains.json
响应示例
{
"service": "my-app",
"version": "20260428T070000Z-abc1234",
"generated_at": "2026-04-28T07:00:00Z",
"domains": [
{ "host": "api.example.com", "priority": 1 },
{ "host": "api2.example.com", "priority": 2 }
]
}
priority 数值越小越优先;客户端建议按顺序尝试,并对结果调用 ping 接口上报。
GET
兜底直读
当 CDN 不可用时(少见),客户端可降级到本系统的源站直接读取(无需签名):
GET https://www.domain-manager.top/api/v1/{slug}/domains.json
GET https://www.domain-manager.top/cdn/{slug}/domains.json
两个端点等价。源站带有 Cache-Control: public, max-age=30,建议仅作为应急回源使用。
客户端示例
TS=$(date +%s)
NONCE=$(openssl rand -hex 8)
BODY='{"reports":[{"host":"api.example.com","success":true,"latency_ms":42}]}'
BODY_HASH=$(printf "%s" "$BODY" | openssl dgst -sha256 | awk '{print $2}')
STR=$(printf "POST\n/api/v1/<slug>/ping\n%s\n%s\n%s" "$TS" "$NONCE" "$BODY_HASH")
SIG=$(printf "%s" "$STR" | openssl dgst -sha256 -hmac "<sign_key>" | awk '{print $2}')
curl -X POST https://www.domain-manager.top/api/v1/<slug>/ping \
-H "Content-Type: application/json" \
-H "X-API-Key: <api_key>" \
-H "X-Timestamp: $TS" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIG" \
-d "$BODY"
import crypto from 'crypto';
const apiKey = '<api_key>';
const signKey = '<sign_key>';
const slug = '<slug>';
const path = `/api/v1/${slug}/ping`;
const body = JSON.stringify({
reports: [{ host: 'api.example.com', success: true, latency_ms: 42 }],
});
const ts = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(8).toString('hex');
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const signStr = ['POST', path, ts, nonce, bodyHash].join('\n');
const signature = crypto.createHmac('sha256', signKey).update(signStr).digest('hex');
await fetch('https://www.domain-manager.top' + path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
'X-Timestamp': ts,
'X-Nonce': nonce,
'X-Signature': signature,
},
body,
});
<?php
$apiKey = '<api_key>';
$signKey = '<sign_key>';
$slug = '<slug>';
$path = "/api/v1/{$slug}/ping";
$body = json_encode([
'reports' => [
['host' => 'api.example.com', 'success' => true, 'latency_ms' => 42],
],
]);
$ts = (string) time();
$nonce = bin2hex(random_bytes(8));
$bodyHash = hash('sha256', $body);
$signStr = implode("\n", ['POST', $path, $ts, $nonce, $bodyHash]);
$sig = hash_hmac('sha256', $signStr, $signKey);
$ch = curl_init('https://www.domain-manager.top' . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"X-API-Key: {$apiKey}",
"X-Timestamp: {$ts}",
"X-Nonce: {$nonce}",
"X-Signature: {$sig}",
],
]);
echo curl_exec($ch);
import time, hmac, hashlib, secrets, json, requests
API_KEY = '<api_key>'
SIGN_KEY = '<sign_key>'
SLUG = '<slug>'
PATH = f'/api/v1/{SLUG}/ping'
body = json.dumps({
'reports': [{'host': 'api.example.com', 'success': True, 'latency_ms': 42}]
}, separators=(',', ':'))
ts = str(int(time.time()))
nonce = secrets.token_hex(8)
bh = hashlib.sha256(body.encode()).hexdigest()
msg = '\n'.join(['POST', PATH, ts, nonce, bh])
sig = hmac.new(SIGN_KEY.encode(), msg.encode(), hashlib.sha256).hexdigest()
r = requests.post(
'https://www.domain-manager.top' + PATH,
data=body,
headers={
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
'X-Timestamp': ts,
'X-Nonce': nonce,
'X-Signature': sig,
},
)
print(r.status_code, r.text)
package main
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strings"
"time"
)
func main() {
apiKey, signKey, slug := "<api_key>", "<sign_key>", "<slug>"
path := "/api/v1/" + slug + "/ping"
body := []byte(`{"reports":[{"host":"api.example.com","success":true,"latency_ms":42}]}`)
ts := fmt.Sprintf("%d", time.Now().Unix())
nb := make([]byte, 8); rand.Read(nb)
nonce := hex.EncodeToString(nb)
bh := sha256.Sum256(body)
msg := strings.Join([]string{"POST", path, ts, nonce, hex.EncodeToString(bh[:])}, "\n")
mac := hmac.New(sha256.New, []byte(signKey)); mac.Write([]byte(msg))
sig := hex.EncodeToString(mac.Sum(nil))
req, _ := http.NewRequest("POST", "https://www.domain-manager.top"+path, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", apiKey)
req.Header.Set("X-Timestamp", ts)
req.Header.Set("X-Nonce", nonce)
req.Header.Set("X-Signature", sig)
resp, _ := http.DefaultClient.Do(req)
fmt.Println(resp.Status)
}
最佳实践
- 客户端启动时优先从 CDN URL 拉取域名清单,同时本地缓存上次成功的清单作为冷启动备份。
- 按
priority升序串行尝试域名,命中后中止;将每次尝试结果(含失败)累计在内存中。 - 每隔 1–5 分钟批量上报一次 ping 结果(最多 100 条),避免单条频繁请求。
- nonce 务必随机;建议 8 字节以上的 hex 或 UUID v4。
- 客户端时钟偏差超过 ±5 分钟会被拒绝,必要时基于响应头校正时钟。
- 遇到 429 时按
Retry-After退避,遇到 401 时检查 sign_key 是否被重置。