MAOMIYUN OPENAPI · V1
把猫咪云接入你的平台
通过服务端接口读取你的实际商品价格、查询余额、创建订单并跟踪进度。所有接口都使用 HTTPS、AppId、HMAC-SHA256 签名和服务器 IP 白名单。
https://maomiyun.com/openapi/v1
快速开始
整个接入只需要四步。接口应用创建成功后,AppSecret 只完整显示一次,请当场保存到你服务器的环境变量中。
- 创建接口应用登录猫咪云,在“个人中心 → 开放接口”创建应用,取得 AppId 和 AppSecret。
- 添加 IP 白名单填写运行对接程序那台服务器的出口公网 IP;不是手机或家里电脑的 IP。
- 计算请求签名每次请求生成 Timestamp、Nonce,并用 AppSecret 计算 HMAC-SHA256。
- 先调 Ping连通测试成功后,再读取商品并创建订单。
请求头与安全要求
除公开文档外,所有 /openapi/v1/* 请求都必须携带以下请求头。
| 请求头 | 是否必填 | 说明 |
|---|---|---|
X-App-Id | 是 | 开发者中心生成的 AppId |
X-Timestamp | 是 | 当前 Unix 秒,允许与服务器时间相差 300 秒 |
X-Nonce | 是 | 每次请求新生成,建议 16 字节以上随机值;10 分钟内不能重复 |
X-Signature-Version | 是 | 固定填写 v1 |
X-Signature | 是 | 64 位小写十六进制 HMAC-SHA256 签名 |
Idempotency-Key | 仅下单 | 8-64 位唯一键,防止网络重试造成重复扣款 |
Content-Type | POST 必填 | application/json |
IP 白名单
白名单只接受精确的公网 IPv4 或 IPv6,每个应用最多 5 个。你在开发者中心添加或删除后立即生效,不需要管理员审核。未添加白名单时,所有接口调用都会返回 IP_NOT_ALLOWED。
签名算法
先按下面的顺序组成待签名字符串。每个字段之间使用一个换行符 \n,末尾不再添加换行。
UPPERCASE_HTTP_METHOD
EXACT_REQUEST_PATH
RFC3986_SORTED_QUERY_STRING
SHA256_HEX_OF_RAW_BODY
X_TIMESTAMP
X_NONCE
然后计算:
signature = hex_lowercase(
HMAC_SHA256(APP_SECRET, canonical_string)
)
查询参数排序
将所有参数按参数名升序排列;参数名相同时按参数值升序。名称和值都按 RFC 3986 百分号编码,再以 & 连接。没有查询参数时,这一行为空。
请求体哈希
必须对实际发送的原始 UTF-8 字节计算 SHA-256。GET 请求没有请求体时,对长度为 0 的空字节串计算,其结果固定为:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
rawBody 字符串,签名和发送都使用同一个值。/ping连通测试验证 AppId、签名、时间、Nonce 和 IP 白名单是否都正确。
请求参数
无
{
"ok": true,
"code": "SUCCESS",
"message": "success",
"data": {
"connected": true,
"server_time": "2026-08-09T08:30:00.000Z"
},
"request_id": "..."
}/account/balance查询账户余额返回接口应用绑定账号的可用余额。金额字段使用十进制字符串,调用方不要用二进制浮点数直接做财务计算。
{
"balance": "126.50",
"currency": "CNY"
}/categories获取商品分类返回当前在售商品对应的平台和分类。
[
{
"category_id": "d387c149a5e2163c",
"platform": "抖音",
"name": "抖音点赞"
}
]/products获取商品列表| 参数 | 必填 | 说明 |
|---|---|---|
platform | 否 | 按平台名称筛选 |
category | 否 | 按二级分类名称筛选 |
keyword | 否 | 按商品名称搜索 |
page | 否 | 页码,默认 1 |
page_size | 否 | 每页数量,默认 20,最大 100 |
unit_price 是当前接口账号的实际售价,已自动计算账号单独价或上级设置的下级价,不是上游成本。/product?product_id=...获取商品详情商品详情会额外返回下单字段 fields。创建订单时,params 的键必须使用字段的 key。
{
"product_id": "p-example",
"name": "抖音点赞",
"platform": "抖音",
"category": "抖音点赞",
"unit_price": "0.0125",
"min_quantity": 10,
"max_quantity": 100000,
"quantity_step": 10,
"status": "available",
"description": "商品说明",
"fields": [
{
"key": "视频地址",
"label": "视频地址",
"type": "text",
"required": true,
"placeholder": "请输入短视频链接"
}
]
}/orders创建订单下单会实时使用绑定账号的实际售价并从余额扣款。请勿提交价格、总金额、用户编号或订单状态。
| JSON 字段 | 必填 | 说明 |
|---|---|---|
client_order_no | 是 | 你系统内的唯一订单号,6-64 位 |
product_id | 是 | 猫咪云商品编号 |
quantity | 是 | 正整数,遵守商品最小值、最大值和递增步长 |
params | 按商品 | 商品详情 fields 要求的下单参数 |
{
"client_order_no": "YOUR-ORDER-20260809-0001",
"product_id": "p-example",
"quantity": 100,
"params": {
"视频地址": "https://v.douyin.com/example/"
}
}幂等规则
每次创建订单都必须携带 Idempotency-Key。网络超时后,请使用相同的请求体和相同的 Key 重试:服务端会返回第一次结果,不会再次扣款或重复派单。同一个 Key 搭配不同请求体会返回 IDEMPOTENCY_CONFLICT。
/order查询单个订单任选一个查询参数:order_id(猫咪云订单号)或 client_order_no(你的订单号)。接口只会返回当前应用绑定账号自己的订单。
/openapi/v1/order?client_order_no=YOUR-ORDER-20260809-0001批量查询可使用 GET /orders?client_order_no=...,不传筛选条件时返回最近 100 笔订单。
订单状态
pending待处理processing处理中done已完成refunding退款中refunded已退款error异常,需关注订单创建后建议每 20-60 秒查询一次;订单进入 done 或 refunded 后停止轮询。
错误码
| HTTP | code | 处理建议 |
|---|---|---|
| 400 | INVALID_ARGUMENT | 检查参数格式、数量和必填字段 |
| 400 | INVALID_JSON | 确保请求体是 UTF-8 JSON 对象 |
| 401 | AUTH_FAILED | 检查 AppId、时间、Nonce、签名和原始请求体 |
| 401 | REPLAY_DETECTED | 重新生成 Nonce 后发起新请求 |
| 403 | IP_NOT_ALLOWED | 将调用服务器的出口公网 IP 加入白名单 |
| 403 | APP_DISABLED | 在开发者中心重新启用应用 |
| 404 | PRODUCT_NOT_FOUND | 刷新商品列表,商品可能已下架 |
| 404 | ORDER_NOT_FOUND | 检查订单号及应用绑定账号 |
| 409 | IDEMPOTENCY_CONFLICT | 为不同下单内容使用新的幂等 Key |
| 409 | CLIENT_ORDER_NO_CONFLICT | 同一个商户订单号不能对应不同商品或数量 |
| 422 | INSUFFICIENT_BALANCE | 充值后使用新的幂等 Key 再下单 |
| 422 | ORDER_PARAMS_INVALID | 按商品详情返回的字段和数量规则修正 |
| 429 | RATE_LIMITED | 按 Retry-After 等待后重试 |
| 502 | SUPPLIER_DISPATCH_FAILED | 保留订单号并查询结果,不要盲目换 Key 重下 |
所有响应都包含唯一的 request_id。需要协助排查时,请把该编号发给客服,不要发送 AppSecret。
完整签名示例
const crypto = require("crypto");
const appId = process.env.MAOMIYUN_APP_ID;
const appSecret = process.env.MAOMIYUN_APP_SECRET;
const baseUrl = "https://maomiyun.com";
function encode(value) {
return encodeURIComponent(value).replace(/[!'()*]/g, c =>
`%${c.charCodeAt(0).toString(16).toUpperCase()}`
);
}
async function request(method, pathname, query = {}, body = null, idempotencyKey = "") {
const entries = Object.entries(query).sort(([ak, av], [bk, bv]) =>
ak === bk ? String(av).localeCompare(String(bv)) : ak.localeCompare(bk)
);
const canonicalQuery = entries.map(([k, v]) => `${encode(k)}=${encode(v)}`).join("&");
const rawBody = body == null ? "" : JSON.stringify(body);
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(18).toString("base64url");
const bodyHash = crypto.createHash("sha256").update(rawBody).digest("hex");
const canonical = [method.toUpperCase(), pathname, canonicalQuery, bodyHash, timestamp, nonce].join("\n");
const signature = crypto.createHmac("sha256", appSecret).update(canonical).digest("hex");
const url = `${baseUrl}${pathname}${canonicalQuery ? `?${canonicalQuery}` : ""}`;
const response = await fetch(url, {
method,
headers: {
"Content-Type": "application/json",
"X-App-Id": appId,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature-Version": "v1",
"X-Signature": signature,
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {})
},
body: rawBody || undefined
});
const result = await response.json();
if (!response.ok) throw new Error(`${result.code}: ${result.message}`);
return result.data;
}
request("GET", "/openapi/v1/account/balance").then(console.log);<?php
$appId = getenv('MAOMIYUN_APP_ID');
$appSecret = getenv('MAOMIYUN_APP_SECRET');
$method = 'GET';
$path = '/openapi/v1/account/balance';
$query = '';
$rawBody = '';
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(18));
$bodyHash = hash('sha256', $rawBody);
$canonical = implode("\n", [$method, $path, $query, $bodyHash, $timestamp, $nonce]);
$signature = hash_hmac('sha256', $canonical, $appSecret);
$ch = curl_init('https://maomiyun.com' . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-App-Id: ' . $appId,
'X-Timestamp: ' . $timestamp,
'X-Nonce: ' . $nonce,
'X-Signature-Version: v1',
'X-Signature: ' . $signature
]
]);
$response = curl_exec($ch);
if ($response === false) throw new RuntimeException(curl_error($ch));
print_r(json_decode($response, true));import hashlib, hmac, os, secrets, time
import requests
app_id = os.environ["MAOMIYUN_APP_ID"]
app_secret = os.environ["MAOMIYUN_APP_SECRET"]
method = "GET"
path = "/openapi/v1/account/balance"
query = ""
raw_body = b""
timestamp = str(int(time.time()))
nonce = secrets.token_urlsafe(18)
body_hash = hashlib.sha256(raw_body).hexdigest()
canonical = "\n".join([method, path, query, body_hash, timestamp, nonce])
signature = hmac.new(app_secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
response = requests.get(
"https://maomiyun.com" + path,
headers={
"X-App-Id": app_id,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature-Version": "v1",
"X-Signature": signature,
},
timeout=15,
)
response.raise_for_status()
print(response.json()["data"])