API 快速上手

Muke Labs 在 https://api.mukelabs.com 提供任务式 REST API。提交生成任务后获得 job id,轮询(或 resolve)直到任务到达终态。所有请求与响应体均为 JSON。

鉴权

API key 在开发者门户 https://portal.muke.name 签发。每个请求都以 Bearer token 携带 key。完整密钥只在创建时展示一次 —— 之后门户只列出其前缀。

门户里创建的每个 key 都带同一组固定 scope —— 不能按 key 选择 scope。key 可设置过期时间,也可随时吊销。

  • Webhook 管理使用独立的 scope(webhook:endpoint:write、webhook:delivery:read、webhook:delivery:replay),单独发放 —— 自助门户 key 不带这些 scope。Webhook 端点在门户中管理;见下文 Webhooks。
Header
Authorization: Bearer <your-api-key>
Scope允许的操作端点
generation:write提交、resolve、取消任务;管理输出产物POST /v1/generations · POST /v1/generations/resolve · POST /v1/generations/{id}/cancel · 产物生命周期端点
generation:read轮询任务状态、回放文本流、读取模型目录GET /v1/generations/{id} · GET /v1/generations/{id}/text-stream · GET /v1/catalog · GET /v1/catalog/status
usage:read用量报表GET /v1/usage

创建生成任务

提交带 JSON 请求体的 POST /v1/generations。API 返回 202 Accepted 和 job id;生成异步执行。

追加 ?wait=true 可让网关为支持同步等待的工具代为短暂轮询。若任务及时到达终态,响应是完整的 Job 对象而非 202 受理。

Request
curl -X POST https://api.mukelabs.com/v1/generations \
  -H "Authorization: Bearer $MUKE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "toolId": "seedream-5-pro",
    "inputMode": "text-to-image",
    "prompt": "a lantern-lit night market, cinematic",
    "params": { "aspect_ratio": "16:9" },
    "references": [],
    "idempotencyKey": "order-42-image-1",
    "subjectId": "user_123"
  }'
Response 202
{
  "jobId": "job_7f3a…",
  "run": { "id": "run_01…", "ordinal": 1 },
  "acceptedAt": "2026-01-01T12:00:00.000Z",
  "policy": { "profileCode": "default", "version": 1 },
  "effectiveRetention": { "kind": "ttl", "ttlDays": 30 },
  "deduplicated": false
}
字段类型必填说明
toolIdstring是模型标识,如 seedream-5-pro。见模型页或 GET /v1/catalog。
inputModestring是如 text-to-image、image-to-video、chat、text-to-speech。必须是该模型允许的输入模式之一。
promptstring是模型不需要提示词时可传空字符串。
paramsobject是模型参数(宽高比、时长、音色…)。必须是 JSON 对象 —— 没有参数时传 {}。
referencesarray是参考媒体 URL。纯文本模式传 []。每项:{url, role?, ordinal?, kind?, expectedContentType?, expectedSha256?};url 必须是绝对 HTTPS。
idempotencyKeystring是≤240 字符,不含空白。每个逻辑任务唯一。
subjectIdstring是你的终端用户标识。配额与策略按 subject 执行。
variantIdstring否模型有多个变体时使用。
messagesarray否对话上下文,仅 inputMode=chat。角色:system、user、assistant。
streamboolean否在支持时请求增量文本输出(仅 chat)。
retentionobject|string|null否{ttlDays: n} 或 "permanent" —— 策略上限内的输出留存覆盖。
webhookEndpointIdstring否把终态事件投递到已注册的 webhook 端点。
subjectPolicyVersioninteger否断言你最后一次观察到的该 subject 策略版本。

轮询结果

GET /v1/generations/{jobId} 返回任务。轮询直到 status 到达终态:succeeded、failed 或 cancelled。建议起点是每 2–5 秒一次请求。

成功的任务暴露 outputs —— 每项带签名 url、contentType、expiresAt,以及用于生命周期操作的 assetHandle。

  • status: queued → running → succeeded | failed | cancelled
  • POST /v1/generations/{jobId}/cancel 取消排队中的任务(运行后返回 JOB_ALREADY_STARTED)。
  • 输出 url 带签名且会过期 —— 在 expiresAt 之前把结果下载到自己的存储。
Response 200
{
  "id": "job_7f3a…",
  "status": "succeeded",
  "outputs": [
    {
      "assetHandle": "asset_…",
      "url": "https://…/signed-download-url",
      "contentType": "image/png",
      "expiresAt": "2026-01-08T12:00:00.000Z"
    }
  ],
  "usage": { "billableUnits": 1, "processingMs": 8200 },
  "createdAt": "2026-01-01T12:00:00.000Z",
  "completedAt": "2026-01-01T12:00:08.000Z"
}

模型目录

GET /v1/catalog 返回你的 key 可用的模型及其允许的输入模式。GET /v1/catalog/status 报告按模型和按输入模式的可用性。

Response 200
{
  "scope": "tenant_base",
  "revision": "rev_…",
  "tools": [
    {
      "toolId": "seedream-5-pro",
      "category": "image",
      "allowedInputModes": ["text-to-image", "image-to-image"]
    }
  ]
}

用量报表

GET /v1/usage 汇总账户在一段时间范围内(最长 366 天)的任务与计费单位。需要 usage:read scope。

  • groupBy:tool 或 subject
  • from / to:带时区的 ISO 8601 时间戳;范围必须为正且 ≤ 366 天
Request
curl "https://api.mukelabs.com/v1/usage?from=2026-01-01T00:00:00Z&to=2026-02-01T00:00:00Z&groupBy=tool" \
  -H "Authorization: Bearer $MUKE_API_KEY"

# -> {
#   "from": "…", "to": "…", "groupBy": "tool",
#   "totals": { "jobs": 12, "billableUnits": 12 },
#   "groups": [{ "key": "seedream-5-pro", "jobs": 12, "billableUnits": 12 }],
#   "truncated": false
# }

幂等与重试

每次提交都要求 idempotencyKey。同一 key 配同一请求体重放的是原始 202 受理(deduplicated: true)—— 超时或网络故障后可安全重试。同一 key 配不同请求返回 409 IDEMPOTENCY_CONFLICT。

POST /v1/generations/resolve 可在不产生重复任务的情况下找回早前请求的受理结果。推荐做法是重发原始生成请求体 —— 网关按提交时完全相同的方式解析并规范化它,然后比对存储的请求哈希。端点应答 {status: "accepted", acceptance: {…}};没有匹配的受理时应答 {status: "fenced"}。fenced 对该 key 是永久的:它记录一条负向栅栏,使迟到的原始 POST 无法再创建任务,该 key 下此后的任何尝试都返回 409 IDEMPOTENCY_CONFLICT。请改用新的 idempotencyKey 重新提交。

也可以只发送 {idempotencyKey, requestHash} —— 不带其他字段 —— 其中 requestHash 是请求规范化 JSON 的 SHA-256 十六进制值。规范化:每层的对象键按字典序排序,值为 undefined 的属性被丢弃,数组顺序保留,数字必须有限,嵌套深度最多 32。哈希针对解析后的请求对象计算,而非原始 JSON 文本。

哈希比对针对的是网关规范化后的请求,与原始请求体在两处不同:references[].expectedContentType 会被转小写(请以小写发送);对 seedance-2 系列模型(seedance-2-0、seedance-2-standard、seedance-2-fast)网关在哈希前注入一个从 inputMode 派生的服务端参数 —— 这些模型务必用原始请求体做 resolve,不要用自算哈希。与存储请求不匹配的哈希返回 409 IDEMPOTENCY_CONFLICT。

requestHash (Node.js)
import { createHash } from "node:crypto";

function canonicalJson(value, depth = 1, ancestors = new Set()) {
  if (value === null || typeof value === "boolean" || typeof value === "string") {
    return JSON.stringify(value);
  }
  if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
  if (value && typeof value === "object") {
    if (depth > 32) throw new Error("Generation request exceeds the maximum JSON depth of 32.");
    if (ancestors.has(value)) throw new Error("Generation request contains a cyclic value.");
    ancestors.add(value);
    try {
      if (Array.isArray(value)) {
        return "[" + value.map((child) => canonicalJson(child, depth + 1, ancestors)).join(",") + "]";
      }
      return "{" + Object.entries(value)
        .filter(([, child]) => child !== undefined)
        .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
        .map(([key, child]) => JSON.stringify(key) + ":" + canonicalJson(child, depth + 1, ancestors))
        .join(",") + "}";
    } finally {
      ancestors.delete(value);
    }
  }
  throw new Error("Generation request contains a non-JSON value.");
}

const requestHash = createHash("sha256")
  .update(canonicalJson(originalRequest), "utf8")
  .digest("hex");

错误

错误返回 { "error": { "code", "message", "retryAfterSeconds?" } } 及相应 HTTP 状态。限流与配额错误可能带 retryAfterSeconds。

HTTPCode含义
400INVALID_JSON / INVALID_REQUEST / VALIDATION_ERROR请求体格式错误,或某字段未通过校验
401INVALID_KEYAPI key 缺失、无效或已停用
403INSUFFICIENT_SCOPE / TOOL_NOT_ALLOWED_BY_POLICYkey 缺少 scope,或 subject 策略禁止该工具
404JOB_NOT_FOUND / NOT_FOUND / WEBHOOK_ENDPOINT_NOT_FOUND未知的任务、路由或 webhook 端点
409IDEMPOTENCY_CONFLICT该幂等键已被用于不同的请求
409JOB_ALREADY_STARTED只有排队中的任务可以取消
409WEBHOOK_ENDPOINT_INVALID_STATE / WEBHOOK_ENDPOINT_LIMIT_REACHED / SUBJECT_POLICY_VERSION_MISMATCHWebhook 端点未激活或未订阅、已达端点数量上限,或断言的 subject 策略版本已变化
413PAYLOAD_TOO_LARGE请求体超过大小限制
415UNSUPPORTED_MEDIA_TYPEContent-Type 必须是 application/json
422CONTENT_REJECTED / PROMPT_TOO_LONG / MEDIA_DURATION_INVALID / REFERENCE_COUNT_INVALID / REFERENCE_MEDIA_INVALID / CAPABILITY_REQUIRED / REQUEST_COST_LIMIT_REACHED请求格式正确但被模型或策略约束拒绝
429SUBJECT_LIMIT_REACHED / TOO_MANY_ACTIVE_GENERATIONS / DAILY_LIMIT_REACHED / TENANT_LIMIT_REACHED达到配额或并发上限 —— 退避后重试
500INTERNAL_ERROR生成未能完成;失败任务自动退款
503GATEWAY_EXECUTION_DISABLED / CATALOG_UNAVAILABLE服务暂时不可用 —— 退避后重试

Webhooks(可选)

除了轮询,还可以注册 webhook 端点并在提交时传 webhookEndpointId。任务到达终态时,网关向你的端点 URL POST 一个签名的 JSON 事件。每个账户最多允许 10 个 pending 或 active 端点。

端点在开发者门户注册与管理。注册只返回一次 signingSecret —— 请立即保存。激活需要握手:Muke Labs 发送一个带 {"event":"webhook.challenge","challenge":"<id>"} 请求体的签名 POST,你的端点必须以 2xx 应答,并在 JSON 体中回显相同的 challenge 值({"challenge":"<id>"})。

提交只能指向已激活且至少订阅了一个终态事件的端点 —— 否则提交被拒绝。同一管理面也以独立 scope 暴露在 REST API 上(自助 key 不带这些 scope):GET/POST /v1/webhook-endpoints、POST /v1/webhook-endpoints/{id}/activate、POST /v1/webhook-endpoints/{id}/secret/rotate、PUT /v1/webhook-endpoints/{id}/subscriptions、DELETE /v1/webhook-endpoints/{id}(scope webhook:endpoint:write);GET /v1/webhook-deliveries 与 GET /v1/webhook-deliveries/{id}(webhook:delivery:read);POST /v1/webhook-deliveries/{id}/replay(webhook:delivery:replay)。

未收到 2xx 应答的投递最多再重试 4 次(共 5 次尝试),大约在 1 分钟、5 分钟、30 分钟和 2 小时,带 ±20% 抖动;429 或 503 应答上的 Retry-After 头会被遵守,上限为一小时。最后一次尝试后投递标记为 exhausted,可在门户中重放。请快速应答 2xx 并异步处理。

信任投递前务必验证:对收到的原始字节(不要重新序列化 JSON)重算签名,拒绝超过约 5 分钟的时间戳,并按 x-muke-event-id 去重。轮换期间继续接受上一个签名密钥 —— 投递会携带其签名所用的密钥版本。

  • 事件类型:generation.succeeded、generation.failed、generation.cancelled
  • data.job 与 GET /v1/generations/{id} 在终态返回的 Job 对象相同,包括 outputs 或 error
Delivery payload
{
  "id": "evt_…",
  "version": "2026-07-22",
  "type": "generation.succeeded",
  "occurredAt": "2026-01-01T12:00:08.000Z",
  "tenantId": "tenant_…",
  "data": {
    "run": { "id": "run_01…", "ordinal": 1 },
    "job": {
      "id": "job_7f3a…",
      "status": "succeeded",
      "outputs": [
        {
          "assetHandle": "asset_…",
          "url": "https://…/signed-download-url",
          "contentType": "image/png",
          "expiresAt": "2026-01-08T12:00:00.000Z"
        }
      ],
      "usage": { "billableUnits": 1, "processingMs": 8200 },
      "createdAt": "2026-01-01T12:00:00.000Z",
      "completedAt": "2026-01-01T12:00:08.000Z"
    }
  }
}
Verify a signature (Node.js)
import { createHmac, timingSafeEqual } from "node:crypto";

// signed = base64url( HMAC-SHA256(secret, "<version>.<timestamp>.<eventId>." || rawBody) )
function signWebhook({ rawBody, eventId, timestamp, version, secret }) {
  return createHmac("sha256", secret)
    .update(`${version}.${timestamp}.${eventId}.`, "utf8")
    .update(rawBody) // exact request bytes — verify before parsing JSON
    .digest("base64url");
}

function verifyWebhook({ rawBody, headers, secretsByVersion, now = new Date(), maxAgeSeconds = 300 }) {
  const eventId = headers["x-muke-event-id"];
  const timestamp = headers["x-muke-timestamp"];
  const version = Number(headers["x-muke-signature-version"]);
  const signature = headers["x-muke-signature"];
  const eventTime = Date.parse(typeof timestamp === "string" ? timestamp : "");
  if (!Number.isFinite(eventTime) || Math.abs(now.getTime() - eventTime) > maxAgeSeconds * 1000) {
    return false; // outside the replay window
  }
  const secret = secretsByVersion[version]; // keep current + previous secret during rotation
  if (!secret || typeof signature !== "string") return false;
  const expected = Buffer.from(signWebhook({ rawBody, eventId, timestamp, version, secret }), "utf8");
  const actual = Buffer.from(signature, "utf8");
  return expected.length === actual.length && timingSafeEqual(expected, actual);
}
Header值
content-typeapplication/json
x-muke-event-id唯一事件标识 —— 用于去重
x-muke-timestampISO-8601 投递时间,如 2026-01-01T12:00:08.123Z
x-muke-signature-version签名密钥版本(整数,从 1 起;轮换时递增)
x-muke-signature对 "{version}.{timestamp}.{eventId}." + 原始请求体字节 的 base64url HMAC-SHA256