API 接入文档

通过 API Key 将 apkgo cloud 集成到你的 CI/CD 流水线中

认证方式

Open API 的 base path 是 /openapi/v1,路径中不需要带 orgId — 组织信息已经绑定在 API Key 上。请求头加上 X-API-Key 即可。

curl -H "X-API-Key: apkgo_your_key_here" \
  https://apkgo.baici.tech/openapi/v1/uploads

Open API 只接受 X-API-Key,不接受 JWT;Dashboard 仍然走 /api/v1/orgs/{orgId}/...。

密钥有效期跟随订阅:API 密钥是专业版及以上的功能,订阅到期(套餐回落到免费版)后,在此页生成的密钥会返回 403,续费后无需重新生成即可恢复。apkgo-cloud login 为 CLI 签发的密钥不受此限制。

上传 APK(三步直传)

APK 字节不再经过 apkgo cloud 服务器,而是直传对象存储。完整流程三步:领上传凭证 → 直传 APK → 创建分发任务。

第 1 步 · 获取上传凭证

curl -X POST \
  -H "X-API-Key: apkgo_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"file_name": "app-release.apk"}' \
  https://apkgo.baici.tech/openapi/v1/uploads/tickets

# → {"data": {"provider": "qiniu",
#             "object_key": "apks/<org>/incoming/<uuid>-app-release.apk",
#             "upload_url": "https://upload-xxx.qiniup.com",
#             "token": "<uptoken>",
#             "expires_at": "..."}}

第 2 步 · 直传 APK 到对象存储

curl -X POST \
  -F "token=<token>" \
  -F "key=<object_key>" \
  -F "file=@app-release.apk" \
  <upload_url>

这一步直连七牛上传节点,带宽与 apkgo cloud 服务器无关。凭证 1 小时内有效,仅能写入该 object_key、限 1GB。

第 3 步 · 创建分发任务(JSON)

curl -X POST \
  -H "X-API-Key: apkgo_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "object_key": "<object_key>",
    "package_name": "com.example.app",
    "version_name": "1.2.0",
    "version_code": 12,
    "release_notes": "修复若干问题",
    "target_stores": ["huawei", "oppo", "vivo", "honor"]
  }' \
  https://apkgo.baici.tech/openapi/v1/uploads

返回 202 Accepted,包含 job ID,上传在后台异步执行。package_name 必填(传 app_id 时可省):服务器不再解析 APK,worker 下载后会按二进制实际包名校验,不一致任务会失败。target_stores 省略时分发到该应用绑定的全部商店;传空数组 [] 则不提交任何商店,只记录版本并更新内测下载链接。可选 sha256 供 worker 校验包体完整性。

支持的商店:huawei、xiaomi、oppo、vivo、honor、meizu、tencent、samsung、googleplay、harmony(鸿蒙,仅接受 .app 整包)。未在控制台为应用绑定凭证的商店会被自动跳过。

CI 脚本示例(bash + jq)

KEY="apkgo_your_key_here"
BASE="https://apkgo.baici.tech/openapi/v1"
APK="app-release.apk"

T=$(curl -s -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d "{\"file_name\":\"$APK\"}" $BASE/uploads/tickets)
OBJ=$(echo "$T" | jq -r .data.object_key)

curl -sf -F "token=$(echo "$T" | jq -r .data.token)" \
  -F "key=$OBJ" -F "file=@$APK" "$(echo "$T" | jq -r .data.upload_url)"

curl -s -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d "{\"object_key\":\"$OBJ\",\"package_name\":\"com.example.app\"}" \
  $BASE/uploads

⚠️ 旧版 -F "apk=@..." multipart 直传接口已移除,继续调用会收到 400 与迁移提示。

替代方案 · 从外部 URL 抓取(file_url)

如果 CI 已把 APK 发布到自有 CDN 或制品库,可以跳过第 1、2 步:把 object_key 换成 file_url,由七牛服务器直接从该 URL 抓取入库,APK 字节同样不经过 apkgo cloud 服务器,也无需你再传一遍。

curl -X POST \
  -H "X-API-Key: apkgo_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "file_url": "https://cdn.example.com/builds/app-release-1.2.0.apk",
    "package_name": "com.example.app",
    "target_stores": ["huawei", "oppo", "vivo", "honor"]
  }' \
  https://apkgo.baici.tech/openapi/v1/uploads

要求与行为:URL 须公网可访问的 http(s)(带签名参数也可以);文件上限 1GB;抓取在创建任务时同步完成,URL 只会被访问这一次——之后的重试、分发、商店拉取全部使用桶内副本,外链过期不影响任务。抓取是同步阻塞的,数百 MB 以上的大文件建议仍走三步直传。file_url 与 object_key 二选一,不能同时传。

查询上传状态

curl -H "X-API-Key: apkgo_your_key_here" \
  https://apkgo.baici.tech/openapi/v1/uploads/{jobId}

状态值:pending → processing → completed 或 failed

API 端点

POST/openapi/v1/uploads/tickets获取直传对象存储的上传凭证
POST/openapi/v1/uploads围绕已直传的 APK 创建分发任务
GET/openapi/v1/uploads列出最近的上传任务
GET/openapi/v1/uploads/{jobId}查询单个任务状态与结果
POST/openapi/v1/uploads/{jobId}/cancel取消任务
POST/openapi/v1/uploads/{jobId}/retry重试失败任务
GET/openapi/v1/apps列出组织下的应用(含 platform,用于定位 iOS 应用)

应用与商店账号的管理只在控制台进行(唯一例外:浏览器扩展「apkgo 助手」用 credential:create 权限的密钥新建商店账号);上传时应用会按 package_name 自动查找或创建,目标商店按「加了账号的商店自动发布」解析——同一商店有多个账号时,需先在控制台「应用 → 发布商店」指定用哪一个。

iOS(.ipa):按 package_name 自动创建只适用于 Android,iOS 应用需先在控制台创建。上传 .ipa 时用 app_id 指向那条 iOS 应用记录(先用 GET /openapi/v1/apps 按 bundle id 找到它),其余流程与 APK 相同。

权限

API Key 携带一组权限(默认 ["upload"])。当前 Open API 所有端点都需要 upload 权限;通配 "*" 等于全开。

错误处理

所有 API 响应格式统一:

// 成功
{"data": { ... }}

// 失败
{"error": "错误信息"}
401API Key 缺失、无效或过期
403权限不足(缺 upload 权限或超出套餐配额)
429请求频率超限(600 次 / 分钟)

Webhook 回调

(专业版及以上)在 API 密钥页面配置 Webhook URL 后,每次上传任务完成(成功或失败)、以及商店审核结果出来(通过/驳回/撤回)时,系统会自动向该 URL 发送 POST 请求。

请求格式

POST https://your-server.com/webhook
Content-Type: application/json
X-Webhook-Signature: sha256=<HMAC-SHA256(body, secret)>

{
  "event": "upload.completed",
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "app_name": "我的应用",
  "package_name": "com.example.app",
  "version_name": "1.2.0",
  "version_code": 12,
  "status": "completed",
  "results": [
    {
      "store_name": "huawei",
      "success": true,
      "duration_ms": 3200
    },
    {
      "store_name": "xiaomi",
      "success": false,
      "error": "credential expired",
      "duration_ms": 1500
    }
  ],
  "timestamp": "2026-04-10T10:00:00Z"
}

事件类型

upload.completed所有商店上传成功
upload.failed至少一个商店上传失败
webhook.test保存 Webhook 地址时发送的验证请求,需返回 HTTP 200 才能保存
review.changed某个商店的审核结果出来了:通过/驳回/撤回(每个商店单独一条)

审核状态变化(review.changed)

review_state 取值 approved/rejected/withdrawn;驳回时 review_detail 带商店返回的原因(如有)。

{
  "event": "review.changed",
  "app_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "app_name": "我的应用",
  "package_name": "com.example.app",
  "store": "huawei",
  "version_name": "1.2.0",
  "version_code": 12,
  "review_state": "rejected",
  "previous_state": "reviewing",
  "review_detail": "应用存在隐私政策问题",
  "timestamp": "2026-04-11T09:30:00+08:00"
}

签名验证

如果配置了签名密钥,请求头 X-Webhook-Signature 包含 HMAC-SHA256 签名。验证示例:

# Python
import hmac, hashlib

def verify(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return signature == f"sha256={expected}"

# Go
func verify(body []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(signature), []byte(expected))
}

# Node.js
const crypto = require("crypto");
function verify(body, signature, secret) {
    const expected = "sha256=" + crypto
        .createHmac("sha256", secret)
        .update(body)
        .digest("hex");
    return signature === expected;
}

接收端示例

# Python (Flask)
@app.route("/webhook", methods=["POST"])
def webhook():
    sig = request.headers.get("X-Webhook-Signature", "")
    if SECRET and not verify(request.data, sig, SECRET):
        return "invalid signature", 401

    data = request.json
    if data["event"] == "upload.completed":
        for r in data["results"]:
            if r["success"]:
                print(f"✅ {r['store_name']}: {r['duration_ms']}ms")
            else:
                print(f"❌ {r['store_name']}: {r['error']}")
    return "ok"

# Go
func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    // verify signature...

    var payload struct {
        Event   string `json:"event"`
        AppName string `json:"app_name"`
        Status  string `json:"status"`
        Results []struct {
            StoreName  string `json:"store_name"`
            Success    bool   `json:"success"`
            Error      string `json:"error"`
            DurationMs int64  `json:"duration_ms"`
        } `json:"results"`
    }
    json.Unmarshal(body, &payload)

    for _, r := range payload.Results {
        if r.Success {
            log.Printf("✅ %s: %dms", r.StoreName, r.DurationMs)
        } else {
            log.Printf("❌ %s: %s", r.StoreName, r.Error)
        }
    }
    w.Write([]byte("ok"))
}

注意事项

  • • 请求超时 10 秒,超时不重试
  • • 返回 2xx 视为成功,其他状态码记录日志但不重试
  • • 签名密钥为空时不发送 X-Webhook-Signature 头
  • • Webhook 在 API 密钥页面底部配置;保存时会先发 webhook.test,返回 200 才能保存
  • • 不支持内网或本机地址(127.0.0.1、10.x、192.168.x 等)