客户 API 开发文档

OfflineIP 客户 API 使用 HMAC-SHA256 签名。所有接口共用同一套路径、请求头、签名规则和响应包络; 下单与续费只从账户余额扣款,API 不提供充值入口,请先在账号内保持足够余额。

接入步骤

  1. 1登录客户中心,进入 我的 API,创建 oip_app_ 开头的 App ID,并立即保存 API Secret。
  2. 2把 OFFLINEIP_API_BASE_URL、OFFLINEIP_API_APP_ID 和 OFFLINEIP_API_SECRET 放进服务端环境变量,不要写进前端页面或公开仓库。
  3. 3先请求 GET /api/v1/account 验证签名,再请求 GET /api/v1/offers 获取可用产品。
  4. 4创建订单或续费时,为一次业务操作生成一个 Idempotency-Key。超时或网络重试必须复用原 key 和完全相同的原始 body。
  5. 5下单成功表示订单已受理。保存返回的 order_no,按响应中的 poll_after_seconds 查询订单详情,直到 poll_after_seconds 为 null;待核对订单请勿重复提交。

关于标识与库存

offer_code、order_no、proxy_id 和 page_token 都是不可解析的公共标识,请原样保存和回传。GET /api/v1/offers 返回的是查询时的快照,不代表预留;下单时会再次核对价格与可用数量。

环境变量

OFFLINEIP_API_BASE_URL=https://www.offlineip.com
OFFLINEIP_API_APP_ID=oip_app_EXAMPLE_APP_ID
OFFLINEIP_API_SECRET=sk_replace-with-your-secret

接口一览

Endpoint用途说明
GET/api/v1/account账号信息读取当前 API Key 所属账号。
GET/api/v1/wallet钱包余额返回可用余额,下单和续费都从这里扣款。
GET/api/v1/offers可购产品支持 country_code、region、line、ip_type、quantity、duration_days 过滤,不分页且不截断。
POST/api/v1/orders创建订单必须带 Idempotency-Key,可一次购买多个地区,只从余额扣款。
GET/api/v1/orders订单列表使用 page_size 与 page_token 稳定翻页;列表不返回 IP 凭据。
GET/api/v1/orders/{order_no}订单详情只能读取当前账号的订单,读取时会自动刷新交付进度。
GET/api/v1/proxiesIP 列表返回已交付的 IP,支持按状态、国家、地区过滤。
GET/api/v1/proxies/{proxy_id}IP 详情只能读取当前账号的 IP。
POST/api/v1/proxies/renew续费必须带 Idempotency-Key,只从余额扣款。proxy_ids 最多 200 个;duration_days 为 30–1080 天且是 30 的整数倍。

响应包络

所有接口返回统一 JSON 包络。成功时 success=true,业务数据在 data; 失败时 code 是稳定错误码,trace_id 可用于联系支持排查。

{
  "success": true,
  "code": "ok",
  "message": "OK",
  "data": {},
  "timestamp": "2026-08-17T12:00:00.000Z",
  "trace_id": "trc_EXAMPLE_TRACE_ID"
}

请求头

Header说明示例
X-API-AppId“我的 API”页面生成的 App ID。oip_app_…
X-API-Timestamp带时区的 ISO 8601 时间戳,允许 5 分钟时钟偏差。2026-08-17T12:00:00Z
X-API-Nonce每次请求唯一的随机值,重复使用会被拒绝。nce_9f2c…
X-API-Signaturecanonical string 的 HMAC-SHA256 小写十六进制值。e3c27a20…
Idempotency-Key创建订单和续费必填。相同 key 加相同 body 会重放首次响应;相同 key 加不同 body 返回冲突。order-demo-001
User-Agent建议设置稳定的服务端标识。OfflineIP-Integration/1.0

Canonical String

签名前把请求拆成 7 行,用换行符 \n 连接。METHOD 大写;host 取请求域名,小写且不含协议与端口;path 含开头斜杠;canonical_query 对 key 和 value 分别 URL 解码后再 rawurlencode,按 key、value 升序排序, 重复 key 不合并,无 query 时留空;body 使用原始请求体的 SHA-256,GET 使用空字符串的 SHA-256。

METHOD
host
path
canonical_query
timestamp
nonce
sha256(body)

Node.js 示例

import crypto from 'node:crypto';

const baseUrl = process.env.OFFLINEIP_API_BASE_URL;
const appId = process.env.OFFLINEIP_API_APP_ID;
const secret = process.env.OFFLINEIP_API_SECRET;

const rfc3986 = (v) =>
  encodeURIComponent(v).replace(/[!'()*]/g, (c) =>
    `%${c.charCodeAt(0).toString(16).toUpperCase()}`);

const canonicalQuery = (raw) =>
  !raw ? '' : raw.split('&')
    .map((pair) => {
      const i = pair.indexOf('=');
      const k = i === -1 ? pair : pair.slice(0, i);
      const v = i === -1 ? '' : pair.slice(i + 1);
      return [rfc3986(decodeURIComponent(k)), rfc3986(decodeURIComponent(v))];
    })
    .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1
      : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0))
    .map(([k, v]) => `${k}=${v}`)
    .join('&');

export async function call(method, path, body) {
  const url = new URL(path, baseUrl);
  const payload = body === undefined ? '' : JSON.stringify(body);
  const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
  const nonce = `nce_${crypto.randomBytes(16).toString('hex')}`;

  const canonical = [
    method.toUpperCase(),
    url.hostname.toLowerCase(),
    url.pathname,
    canonicalQuery(url.search.replace(/^\?/, '')),
    timestamp,
    nonce,
    crypto.createHash('sha256').update(payload).digest('hex'),
  ].join('\n');

  const headers = {
    'X-API-AppId': appId,
    'X-API-Timestamp': timestamp,
    'X-API-Nonce': nonce,
    'X-API-Signature': crypto.createHmac('sha256', secret)
      .update(canonical).digest('hex'),
    Accept: 'application/json',
    'User-Agent': 'OfflineIP-Integration/1.0',
  };
  if (payload) headers['Content-Type'] = 'application/json';

  const res = await fetch(url, { method, headers, body: payload || undefined });
  return res.json();
}

创建订单

POST /api/v1/orders
Idempotency-Key: order-demo-001

{
  "duration_days": 30,
  "items": [
    { "offer_code": "off_...", "quantity": 2 },
    { "offer_code": "off_...", "quantity": 1, "cidr": "104.206.10.0/24" }
  ]
}

items 最多 50 行,整笔订单总数量不超过 2000。duration_days 支持 30 至 1080 天,必须是 30 的整数倍;购买还须满足产品返回的支持时长。 旧版单地区格式 offer_code + quantity + duration_days 继续兼容,并支持可选 cidr;顶层 offer_code、quantity、cidr 均不能与 items 同时出现。

稳定分页

GET /api/v1/proxies?page_size=100

{
  "proxies": [],
  "page_size": 100,
  "has_more": true,
  "next_page_token": "opaque-token-from-response"
}

请求下一页时把返回的 next_page_token 原样放入 page_token,并保持相同的 page_size。token 绑定当前账号与资源类型,篡改、过期或跨账号使用都会被拒绝。

订单状态与轮询

{
  "order_no": "OIP20260817120000ABCD1234",
  "status": "procuring",
  "payment_status": "paid",
  "delivery": {
    "expected_count": 3,
    "delivered_count": 1,
    "remaining_count": 2,
    "complete": false,
    "available": true
  },
  "poll_after_seconds": 2,
  "refund": null,
  "proxies": []
}

paid 或 procuring 表示处理中。当 poll_after_seconds 是整数时,等待返回的秒数后再查询;为 null 或订单进入终态时停止轮询。 只有 delivery.available=true 时,详情中的 proxies 才可交付使用。partial 表示部分交付,已返回且可用的 IP 可先使用;remaining_count 表示尚未交付数量,未知时为 null。 部分交付也可能已停止轮询,请始终遵循 poll_after_seconds。manual_review 表示结果待核对,停止自动轮询并联系客服,切勿重新下单。 已确认的采购失败会按实际未交付数量退回余额;部分退款时 payment_status 为 partially_refunded,已交付的 IP 保留可用;退款金额以 refund 为准。 全额退款时状态为 refunded。

常见错误

  • invalid_signature

    签名缺失、App ID 错误、canonical string 拼接不一致,或使用了错误的 Secret。

  • stale_timestamp

    时间戳没有时区、格式不是 ISO 8601,或与服务器相差超过 5 分钟。

  • replayed_nonce

    同一个 Key 下重复使用了相同 nonce,每次请求都要生成新的随机值。

  • idempotency_key_required

    创建订单或续费缺少 Idempotency-Key。

  • idempotency_key_conflict

    同一个 Idempotency-Key 已用于不同的请求体。

  • idempotency_processing

    相同操作仍在处理中,保持相同 key 和 body 稍后重试。

  • idempotency_replay_failed

    首次响应还没有安全落盘,无法重放。保持相同 key 与 body 稍后重试。

  • insufficient_scope

    创建订单和续费需要 Key 具备 write 权限;读接口不校验权限。控制台创建的 Key 默认两者都有。

  • api_key_revoked

    API Key 已停用,请换用有效的 Key。

  • validation_error

    字段、数量、时长或组合不符合约束,按 details 修正后重试。

  • order_not_found / proxy_not_found

    标识不存在,或不属于当前账号。

  • insufficient_balance

    钱包余额不足,订单没有创建,也没有扣款。details 给出 balance_cents 与 required_cents。

  • offer_unavailable

    所选线路已下架或库存不足,订单未成交;若已扣款会全额退回余额。

  • supply_unavailable

    线路暂时无法出货,订单未成交,款项已全额退回余额。可稍后重试。

  • invalid_request

    数量或时长超出该线路的可售范围,订单未成交。

  • invalid_page_token

    分页 token 过期、被修改,或用于错误的账号与资源。请从第一页重新获取。

  • rate_limited

    请求过快,读取 Retry-After 后重试。

  • service_unavailable

    下单未成功,款项已退回余额,请稍后重试。

  • operation_failed

    采购未完成,款项已退回余额。保存 trace_id 并联系支持。