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。
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 受理。
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"
}'{
"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
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| toolId | string | 是 | 模型标识,如 seedream-5-pro。见模型页或 GET /v1/catalog。 |
| inputMode | string | 是 | 如 text-to-image、image-to-video、chat、text-to-speech。必须是该模型允许的输入模式之一。 |
| prompt | string | 是 | 模型不需要提示词时可传空字符串。 |
| params | object | 是 | 模型参数(宽高比、时长、音色…)。必须是 JSON 对象 —— 没有参数时传 {}。 |
| references | array | 是 | 参考媒体 URL。纯文本模式传 []。每项:{url, role?, ordinal?, kind?, expectedContentType?, expectedSha256?};url 必须是绝对 HTTPS。 |
| idempotencyKey | string | 是 | ≤240 字符,不含空白。每个逻辑任务唯一。 |
| subjectId | string | 是 | 你的终端用户标识。配额与策略按 subject 执行。 |
| variantId | string | 否 | 模型有多个变体时使用。 |
| messages | array | 否 | 对话上下文,仅 inputMode=chat。角色:system、user、assistant。 |
| stream | boolean | 否 | 在支持时请求增量文本输出(仅 chat)。 |
| retention | object|string|null | 否 | {ttlDays: n} 或 "permanent" —— 策略上限内的输出留存覆盖。 |
| webhookEndpointId | string | 否 | 把终态事件投递到已注册的 webhook 端点。 |
| subjectPolicyVersion | integer | 否 | 断言你最后一次观察到的该 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 之前把结果下载到自己的存储。
{
"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 报告按模型和按输入模式的可用性。
{
"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 天
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。
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。
| HTTP | Code | 含义 |
|---|---|---|
| 400 | INVALID_JSON / INVALID_REQUEST / VALIDATION_ERROR | 请求体格式错误,或某字段未通过校验 |
| 401 | INVALID_KEY | API key 缺失、无效或已停用 |
| 403 | INSUFFICIENT_SCOPE / TOOL_NOT_ALLOWED_BY_POLICY | key 缺少 scope,或 subject 策略禁止该工具 |
| 404 | JOB_NOT_FOUND / NOT_FOUND / WEBHOOK_ENDPOINT_NOT_FOUND | 未知的任务、路由或 webhook 端点 |
| 409 | IDEMPOTENCY_CONFLICT | 该幂等键已被用于不同的请求 |
| 409 | JOB_ALREADY_STARTED | 只有排队中的任务可以取消 |
| 409 | WEBHOOK_ENDPOINT_INVALID_STATE / WEBHOOK_ENDPOINT_LIMIT_REACHED / SUBJECT_POLICY_VERSION_MISMATCH | Webhook 端点未激活或未订阅、已达端点数量上限,或断言的 subject 策略版本已变化 |
| 413 | PAYLOAD_TOO_LARGE | 请求体超过大小限制 |
| 415 | UNSUPPORTED_MEDIA_TYPE | Content-Type 必须是 application/json |
| 422 | CONTENT_REJECTED / PROMPT_TOO_LONG / MEDIA_DURATION_INVALID / REFERENCE_COUNT_INVALID / REFERENCE_MEDIA_INVALID / CAPABILITY_REQUIRED / REQUEST_COST_LIMIT_REACHED | 请求格式正确但被模型或策略约束拒绝 |
| 429 | SUBJECT_LIMIT_REACHED / TOO_MANY_ACTIVE_GENERATIONS / DAILY_LIMIT_REACHED / TENANT_LIMIT_REACHED | 达到配额或并发上限 —— 退避后重试 |
| 500 | INTERNAL_ERROR | 生成未能完成;失败任务自动退款 |
| 503 | GATEWAY_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
{
"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"
}
}
}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-type | application/json |
| x-muke-event-id | 唯一事件标识 —— 用于去重 |
| x-muke-timestamp | ISO-8601 投递时间,如 2026-01-01T12:00:08.123Z |
| x-muke-signature-version | 签名密钥版本(整数,从 1 起;轮换时递增) |
| x-muke-signature | 对 "{version}.{timestamp}.{eventId}." + 原始请求体字节 的 base64url HMAC-SHA256 |