API REFERENCE · v1

域名管理 API 文档

提供给客户端 SDK 集成:高速获取域名清单、上报 ping 结果。所有接口基于 HTTPS + HMAC-SHA256 签名。

Base URL https://www.domain-manager.top

概览

系统将每个网站主体抽象为一个「服务」。每个服务拥有独立的 api_keysign_key,并绑定多个域名。客户端通过:

  1. 从 CDN URL 高速读取该服务最新的域名清单 JSON
  2. 客户端 SDK 对清单中的域名进行 ping 测试,上报到本系统
  3. 后台基于上报数据分析可用性与时延
写入
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 含义
401unauthorized缺少签名头 / 时间戳过期 / nonce 重放 / 签名不匹配
403forbiddenapi_key 不属于该 slug
422validation_failed参数校验失败(详见 message)
429rate_limited限流触发,Retry-After 提示等待秒数
404not_found服务不存在或已停用
POST

/api/v1/{slug}/ping

客户端批量上报对域名的 ping 结果。需要签名认证。

请求体

{
  "reports": [
    {
      "host": "api.example.com",
      "success": true,
      "latency_ms": 42,
      "error_code": null,
      "ts": 1740000000
    }
  ]
}
字段 类型 说明
reportsarray(1-100)单次最多 100 条
reports[].hoststring域名(必须出现在该服务的清单中或子域)
reports[].successbool本次 ping 是否成功
reports[].latency_msint (可选)耗时毫秒,0–600000
reports[].error_codestring (可选)失败原因短码,如 timeout / dns / refused
reports[].tsint (可选)上报对应的 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 是否被重置。