客户 API 开发文档
OfflineIP 客户 API 使用 HMAC-SHA256 签名。所有接口共用同一套路径、请求头、签名规则和响应包络; 下单与续费只从账户余额扣款,API 不提供充值入口,请先在账号内保持足够余额。
接入步骤
- 1登录客户中心,进入
我的 API,创建oip_app_开头的 App ID,并立即保存 API Secret。 - 2把
OFFLINEIP_API_BASE_URL、OFFLINEIP_API_APP_ID和OFFLINEIP_API_SECRET放进服务端环境变量,不要写进前端页面或公开仓库。 - 3先请求
GET /api/v1/account验证签名,再请求GET /api/v1/offers获取可用产品。 - 4创建订单或续费时,为一次业务操作生成一个
Idempotency-Key。超时或网络重试必须复用原 key 和完全相同的原始 body。 - 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/proxies | IP 列表 | 返回已交付的 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-Signature | canonical 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_revokedAPI 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 并联系支持。