PuppyIP / Developer Docs
客户 API 开发文档
PuppyIP 客户 API 使用 HMAC-SHA256 签名。沙盒和正式环境使用相同路径、请求头、签名规则和响应包络;联调完成后,只需要替换 Base URL、App ID 和 API Secret。
接入步骤
- 1. 正式环境“我的 API”页面提供沙盒联调入口。进入测试环境后,登录客户中心,进入“我的 API”,创建
sbx_app_开头的 App ID,并立即保存 API Secret。 - 2. 把
PUPPYIP_API_BASE_URL、PUPPYIP_API_APP_ID和PUPPYIP_API_SECRET放进服务端环境变量,不要写进前端页面。 - 3. 先请求
GET /api/v1/account验证签名,再请求GET /api/v1/offers获取可用产品。 - 4. 创建订单或续费时,每次请求都生成新的
Idempotency-Key,避免网络重试造成重复扣款。 - 5. 沙盒调通后,回到正式环境创建
live_app_开头的 App ID。上线只替换正式环境变量。复制代码示例后,只需要替换环境变量,签名函数、请求头和接口路径不需要重写。
环境切换
建议把连接信息放在环境变量中。测试使用 PUPPYIP_API_BASE_URL=https://sandbox.puppyip.com 和 sbx_app_ 开头的 App ID;上线改为 PUPPYIP_API_BASE_URL=https://puppyip.com 和 live_app_ 开头的 App ID。测试和上线不需要改请求头、签名函数或接口路径。
PUPPYIP_API_BASE_URL=https://sandbox.puppyip.com
PUPPYIP_API_APP_ID=sbx_app_EXAMPLE_SANDBOX_APP_ID
PUPPYIP_API_SECRET=replace-with-your-secret
当前可用接口
| Endpoint | 用途 | 说明 |
|---|---|---|
| GET /api/v1/account | 账号信息 | 读取当前 API Key 所属账号。 |
| GET /api/v1/wallet | 钱包余额 | 生产环境返回真实钱包余额,沙盒返回测试余额。 |
| GET /api/v1/offers | 可购产品 | 可按地区、城市、线路、数量和时长筛选。 |
| POST /api/v1/orders | 创建订单 | 必须带 Idempotency-Key,只从钱包余额扣款。 |
| GET /api/v1/orders/{order_no} | 订单详情 | 只能读取当前账号自己的订单。 |
| GET /api/v1/proxies | IP 列表 | 返回当前账号已交付的 IP。 |
| GET /api/v1/proxies/{proxy_id} | IP 详情 | 只能读取当前账号自己的 IP。 |
| POST /api/v1/proxies/renew | 续费 | 必须带 Idempotency-Key,只从钱包余额扣款。 |
响应包络
所有接口都返回统一 JSON 包络。成功时 success=true,业务数据在 data;失败时 code 是稳定错误码,trace_id 可用于联系支持排查。
{
"success": true,
"code": "ok",
"message": "OK",
"data": {},
"timestamp": "2026-06-28T12:00:00+00:00",
"trace_id": "trc_EXAMPLE_TRACE_ID"
}
请求头
| Header | 说明 | 示例 |
|---|---|---|
| X-API-AppId | 账号页生成的 App ID。 | live_app_EXAMPLE_LIVE_APP_ID |
| X-API-Timestamp | 绝对时间戳,必须带时区,允许 5 分钟内的时钟偏差。 | 2026-06-28T12:00:00+00:00 |
| X-API-Nonce | 每次请求唯一随机值,重复使用会被拒绝。 | nonce-empty-body |
| X-API-Signature | 用 API Secret 对 canonical string 计算出的 HMAC-SHA256 小写十六进制值。 | e3c27a2055ad38f16861790294802b9fac81e02e8d72fbeaa87299598785832d |
| Idempotency-Key | 创建订单和续费必填。相同 key 加相同原始 body 会重放第一次响应;相同 key 加不同 body 会返回冲突。 | order-demo-001 |
Canonical String
签名前先把请求拆成 7 行,用换行符 \n 连接:
METHOD
host
path
canonical_query
timestamp
nonce
sha256(body)
METHOD 使用大写,例如 GET 或 POST。
host 来自 Base URL 的主机名,使用小写,不包含协议。
path 必须包含开头斜杠,例如 /api/v1/account。
canonical_query 对 query 的 key 和 value 分别 URL 解码后再 rawurlencode,按 key、value 升序排序,重复 key 不合并。没有 query 时这一行留空。
body 使用原始请求体计算 SHA-256。GET 请求没有 body 时,使用空字符串的 SHA-256。
GET /api/v1/account 固定示例
GET
puppyip.com
/api/v1/account
2026-06-28T12:00:00+00:00
nonce-empty-body
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
上面这组固定示例使用文档测试 Secret puppyip-docs-test-secret 计算出的签名是
e3c27a2055ad38f16861790294802b9fac81e02e8d72fbeaa87299598785832d。
代码示例
以下示例使用环境变量。请不要把 API Secret 写入前端页面、公开仓库或日志。
curl
curl "$PUPPYIP_API_BASE_URL/api/v1/account" \
-H "X-API-AppId: $PUPPYIP_API_APP_ID" \
-H "X-API-Timestamp: 2026-06-28T12:00:00+00:00" \
-H "X-API-Nonce: nonce-empty-body" \
-H "X-API-Signature: e3c27a2055ad38f16861790294802b9fac81e02e8d72fbeaa87299598785832d"
PHP
$baseUrl = rtrim(getenv('PUPPYIP_API_BASE_URL') ?: 'https://sandbox.puppyip.com', '/');
$appId = getenv('PUPPYIP_API_APP_ID') ?: '';
$secret = getenv('PUPPYIP_API_SECRET') ?: '';
$method = 'GET';
$path = '/api/v1/account';
$query = '';
$body = '';
$timestamp = gmdate('Y-m-d\TH:i:s\Z');
$nonce = 'nce_'.bin2hex(random_bytes(16));
$host = strtolower(parse_url($baseUrl, PHP_URL_HOST) ?: '');
$canonical = implode("\n", [
$method,
$host,
$path,
$query,
$timestamp,
$nonce,
hash('sha256', $body),
]);
$signature = hash_hmac('sha256', $canonical, $secret);
$headers = [
'X-API-AppId: '.$appId,
'X-API-Timestamp: '.$timestamp,
'X-API-Nonce: '.$nonce,
'X-API-Signature: '.$signature,
];
Node.js
import crypto from 'node:crypto';
const baseUrl = process.env.PUPPYIP_API_BASE_URL ?? 'https://sandbox.puppyip.com';
const appId = process.env.PUPPYIP_API_APP_ID ?? '';
const secret = process.env.PUPPYIP_API_SECRET ?? '';
const method = 'GET';
const url = new URL('/api/v1/account', baseUrl);
const query = '';
const body = '';
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
const nonce = `nce_${crypto.randomBytes(16).toString('hex')}`;
const canonical = [
method,
url.host.toLowerCase(),
url.pathname,
query,
timestamp,
nonce,
crypto.createHash('sha256').update(body).digest('hex'),
].join('\n');
const signature = crypto
.createHmac('sha256', secret)
.update(canonical)
.digest('hex');
const headers = {
'X-API-AppId': appId,
'X-API-Timestamp': timestamp,
'X-API-Nonce': nonce,
'X-API-Signature': signature,
};
业务请求示例
下单和续费都必须使用钱包余额,并带上 Idempotency-Key。签名时,body hash 必须使用发送出去的原始 JSON 字符串。
创建订单
POST /api/v1/orders
Idempotency-Key: order-demo-001
{
"offer_code": "sandbox_us_static",
"quantity": 1,
"duration_days": 30
}
续费 IP
POST /api/v1/proxies/renew
Idempotency-Key: renew-demo-001
{
"proxy_ids": ["SANDBOX_PROXY_20260629080000_001"],
"duration_days": 30
}
GET /api/v1/offers 获取正式 offer_code,并用 GET /api/v1/proxies 获取正式 proxy_id。不要把沙盒返回的 ID 写进正式服务。
常见错误
- invalid_signature
- 签名缺失、App ID 错误、canonical string 拼接不一致,或使用了错误的 API Secret。
- stale_timestamp
- 时间戳没有时区、格式不是 ISO 8601,或客户端时间与服务器相差超过 5 分钟。
- replayed_nonce
- 同一个 API Key 下重复使用了相同 nonce。每次请求都应该生成新的随机 nonce。
- idempotency_key_required
- 创建订单或续费请求缺少 Idempotency-Key。
- idempotency_key_conflict
- 同一个 Idempotency-Key 已经用于不同请求体。
- insufficient_scope
- API Key 没有所需权限,或请求来源 IP 不在允许范围内。
- insufficient_wallet_balance
- 钱包余额不足,创建订单或续费没有扣款成功。
- unsupported_operation
- 该操作不属于客户 API v1,例如修改代理账号密码或切换 IP。