PuppyIP

PuppyIP / Developer Docs

客户 API 开发文档

PuppyIP 客户 API 使用 HMAC-SHA256 签名。沙盒和正式环境使用相同路径、请求头、签名规则和响应包络;联调完成后,只需要替换 Base URL、App ID 和 API Secret。

接入步骤

  1. 1. 正式环境“我的 API”页面提供沙盒联调入口。进入测试环境后,登录客户中心,进入“我的 API”,创建 sbx_app_ 开头的 App ID,并立即保存 API Secret。
  2. 2. 把 PUPPYIP_API_BASE_URLPUPPYIP_API_APP_IDPUPPYIP_API_SECRET 放进服务端环境变量,不要写进前端页面。
  3. 3. 先请求 GET /api/v1/account 验证签名,再请求 GET /api/v1/offers 获取可用产品。
  4. 4. 创建订单或续费时,每次请求都生成新的 Idempotency-Key,避免网络重试造成重复扣款。
  5. 5. 沙盒调通后,回到正式环境创建 live_app_ 开头的 App ID。上线只替换正式环境变量。复制代码示例后,只需要替换环境变量,签名函数、请求头和接口路径不需要重写。
沙盒环境只产生测试余额、测试订单和测试 IP。正式环境会使用真实钱包余额扣款;API 不提供充值或外部支付入口,请先在 PuppyIP 账号内保持足够余额。

环境切换

建议把连接信息放在环境变量中。测试使用 PUPPYIP_API_BASE_URL=https://sandbox.puppyip.comsbx_app_ 开头的 App ID;上线改为 PUPPYIP_API_BASE_URL=https://puppyip.comlive_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
不要写死沙盒 offer_code。联调时也应先请求 GET /api/v1/offers 获取可用产品,再把返回的 offer_code 传给 POST /api/v1/orders。上线前重新请求 GET /api/v1/offers,并使用生产环境返回的 offer_code。
API Key 只代表 PuppyIP 客户 API 凭证。沙盒 Key 和正式 Key 不能混用;客户请求和文档中只使用 PuppyIP 发放的 App ID 和 API Secret。下单和续费仅使用账号钱包余额,API 不提供单独充值或外部支付入口。

当前可用接口

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 使用大写,例如 GETPOST

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。